Skip to content

React 19, Mantine 9, un'istanza Open-Meteo propria, e «non misurato» che smette di sembrare «tranquillo» - #144

Merged
gzileni merged 8 commits into
mainfrom
react19-mantine9
Sep 30, 2026
Merged

gzileni merged 8 commits into
mainfrom
react19-mantine9

Conversation

@gzileni

@gzileni gzileni commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Quattro cose, tutte partite dalla stessa domanda: "parti con la 9 e verifica la UI, i dati alluvione e incendi sono vuoti".

1. React 19 + Mantine 9

Due rotture sole, entrambe meccaniche:

  • React 19 ha tolto il namespace globale JSX. Le 39 annotazioni JSX.Element su 25 file importano il tipo da "react".
  • Mantine 9 ha rinominato la prop di Collapse: in → expanded.

2. Perché alluvione e incendio davano zero

Open-Meteo rifiutava ogni chiamata: 429 Daily API request limit exceeded, 526 integration.degraded in 26 ore. I motori degradavano come prescritto — fire_weather: null, rain_mm: null — e nessuno inventava un numero.

Il volume era sopra il tetto per costruzione: ~diecimila località a ogni tick orario, ventiquattro volte al giorno, contro un piano gratuito da 10.000 chiamate.

Due dei tre consumi chiedevano più volte al giorno un dato giornaliero:

  • la catena FWI avanza di giorno, lo sweep gira di ora. Se il giorno bersaglio è già in fwi_state per tutti i nodi si rilegge invece di ricamminarlo, e la finestra scaricata copre solo i giorni mancanti. Da 24 fetch da 7 giorni a 1 da 2.
  • la previsione di portata è GloFAS, che gira una volta al giorno. Cache di sei ore. Un degrado non si scrive mai: un 429 isolato terrebbe l'AOI a secco.

3. Un'istanza Open-Meteo propria

Open-Meteo è open source e gira in Docker: ospitarlo toglie il tetto invece di spostarlo, nella direzione che il progetto ha già scelto.

Profilo compose meteo (make meteo-up, meteo-check, meteo-sync). Previsione e archivio passano da OPENMETEO__FORECAST_URL / __ARCHIVE_URL, vuote per default. Portata e onde restano pubbliche: GloFAS non è sul bucket AWS di Open-Meteo e non si può ospitare.

Verificato con uno sweep vero sul Friuli, una delle regioni che la catena aveva perso per prima:

executor.meteo_fetch   degraded=false  rain_nodes=35  api_30_mm=145.9
fwi_update.done        nodes=35  advanced=35  with_chain=35     (era 0, 0)
executor.risk_scoring  cells=8282  top_score=0.2572             (era 0.0000)

Una porta pubblicata su 127.0.0.1 non è raggiungibile da un altro container — host.docker.internal risolve al gateway. Il servizio sta sulla rete limen_default; la 8085 resta per make meteo-check dall'host.

4. «Non misurato» non è «nessun pericolo» — chiude #143

Un'integrazione degradata dà un risultato neutro, e per un motore moltiplicativo il neutro è zero: sulla mappa, in fondo alla scala YlOrRd, accanto alla scritta della classe più tranquilla. Era l'unico posto in cui questo sistema sbagliava in direzione rassicurante.

Non è ricavabile dal punteggio — una cella calma vale zero anche lei. Risponde il breakdown: measured(), la quarta proiezione accanto a components(), factors_payload() e predisposition(). Il flag viaggia su risk_assessments e latest_risk (053), sulle tre sorgenti delle tile (054, 055) e sul rollup per comune (bool_or: basta una cella).

Esce dall'indice di attenzione, ed è la parte che conta: contarlo come «sotto soglia» abbassa il comune due volte, e la seconda è la peggiore — due pericoli concomitanti perderebbero l'incremento per colpa di un terzo ignoto. Niente di misurato ⇒ attention è None, mai 0.

Sulla mappa si disegna con lo stesso grigio delle celle non valutate: dicono la stessa cosa, e una sesta tinta renderebbe illeggibile una scala a cinque classi.

E i numeri si spiegano

Nella colonna convivevano due scale senza dirlo: il punteggio va da 0 a 1, l'attenzione da 0 a 3 — un 1,37 nudo accanto a celle da 0,40 non si sa interpretare. Ora il numero grande ha la parola attenzione accanto, ogni pericolo mostra il suo punteggio oltre alla classe (Frana 0,40 moderato), i tooltip danno la banda che definisce «moderato», la lista delle celle dichiara la sua scala, e un ? apre le tre definizioni per intero. Numeri in formato italiano, che prima non lo erano.


Gate: 1032 test Python, 119 frontend, ruff + format + mypy --strict puliti, build a 872 moduli.

Chiude #140 (assorbita), #142 e #143.

🤖 Generated with Claude Code

gzileni and others added 2 commits September 29, 2026 13:22
React 19 ha tolto il namespace globale `JSX`, quindi le 39 annotazioni
`JSX.Element` sparse su 25 file vanno importate esplicitamente da "react".
Mantine 9 ha rinominato la prop di Collapse: `in` e' diventato `expanded`.

Nient'altro si e' rotto: tsc pulito, 113 test verdi, eslint a zero warning,
build a 872 moduli.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…olta al giorno

Alluvione e incendio danno zero su ogni cella d'Italia, e non perche' il
territorio sia calmo: Open-Meteo risponde 429 "Daily API request limit
exceeded" a *tutte* le chiamate. Il breakdown lo dice gia' onestamente —
`fire_weather: null`, `rain_mm: null`, `discharge_ratio: null` — e i motori
degradano come devono. Il difetto sta a monte, nel volume.

Ogni tick orario chiedeva a Open-Meteo circa diecimila localita':
1446 nodi FWI per quattro variabili per sette giorni, piu' 8538 nodi
alluvione sulle due griglie pluviale e fluviale. Ventiquattro volte al
giorno, contro un tetto gratuito di 10.000 chiamate.

Due dei tre consumi chiedevano piu' volte al giorno un dato che al giorno
cambia una volta sola:

- la catena FWI avanza di giorno. Se il giorno bersaglio e' gia' scritto in
  `fwi_state` per tutti i nodi, ora si rilegge invece di ricamminarlo; e la
  finestra scaricata copre solo i giorni che qualche nodo deve ancora
  camminare, non `max_gap_days` a prescindere. Da 24 fetch da 7 giorni a
  1 da 2.
- la previsione di portata e' GloFAS, che gira una volta al giorno. Passa
  dalla stessa cache del riferimento, con TTL di sei ore. Una risposta tutta
  vuota non si scrive: un singolo 429 terrebbe l'AOI a secco per sei ore.

Resta fuori la griglia pluviale, che e' l'unico segnale che si muove
davvero dentro la giornata: 8538 localita' per 24 tick sono ancora sopra il
tetto gratuito da sole, e la scelta fra chiave a pagamento, Open-Meteo
auto-ospitato, passo piu' largo o cadenza piu' lenta e' operativa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be47c03374

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +510 to +512
if any(r is not None for r in ratios):
try:
await cache.set_json(key, ratios, ttl_seconds=_FLUVIAL_TTL_SECONDS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid caching partially degraded forecast grids

When at least one 100-node batch succeeds but a later batch times out, returns invalid JSON, or hits the API limit, fetch_grid substitutes empty points for the failed batch and _fluvial_by_node converts them to None. Since this condition accepts the result as soon as any other node has a ratio, the mixed response is cached for six hours; subsequent sweeps therefore cannot recover the failed nodes, and their missing discharge is treated as a zero fluvial trigger. Cache only responses known to have completed without batch degradation, rather than using the presence of one valid ratio as the success criterion.

Useful? React with 👍 / 👎.

Comment on lines +139 to +141
def _fluvial_key(nodes: list[tuple[float, float]]) -> str:
fingerprint = ";".join(f"{lon:.4f},{lat:.4f}" for lon, lat in nodes)
return f"flood:fluvial_forecast:{sha256(fingerprint.encode()).hexdigest()[:32]}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include the discharge reference in the forecast cache key

For the same node lattice, the cached values also depend on the supplied reference, because _fluvial_by_node divides each forecast peak by that reference. This key fingerprints only the nodes, so a configuration rollout that changes reference_percentile or reference_window_days—or two callers using different reference definitions—will reuse ratios calculated with the previous denominator for up to six hours, despite _reference_key correctly separating those configurations. Include the reference definition or a fingerprint of the reference values in this key, or cache raw forecast discharge instead.

Useful? React with 👍 / 👎.

gzileni and others added 3 commits September 29, 2026 13:53
…arisce

Open-Meteo e' open source e gira in Docker: ospitarlo toglie il limite di
10.000 chiamate al giorno che ha spento alluvione e incendio su tutta
l'Italia, e lo fa nella direzione che questo progetto ha gia' scelto —
self-hosted, nessun cloud.

Cosa cambia. Cinque URL erano costanti di modulo; ora previsione e archivio
passano da `OPENMETEO__FORECAST_URL` / `__ARCHIVE_URL`, vuote per default,
quindi un deployment che non ospita niente non configura niente e parla con
l'API pubblica come prima. Da li' passa il 95 % del traffico: le due griglie
FWI e pioggia per nodo, piu' la griglia pluviale dell'alluvione.

Cosa non cambia, e non per dimenticanza. **Portata e onde restano
pubbliche.** GloFAS non e' pubblicato sul bucket AWS di Open-Meteo — "climate,
flood, satellite and ensemble models are not published due to their immense
size" — quindi il ramo fluviale non si puo' ospitare comunque; ed e' il
consumo piccolo, ora in cache sei ore. Le onde sono uno scalare per AOI,
venti chiamate l'ora, che non sono mai state il problema. Dare loro una
manopola sarebbe la messa in conto del futuro che CLAUDE.md vieta.

Il profilo compose `meteo` ha un progetto suo e non condivide la rete con lo
stack operativo: i container la raggiungono su `host.docker.internal:8085`,
come il gateway LiteLLM, e la porta e' legata al loopback come ogni altra.

Verificato sull'istanza appena avviata, a volume vuoto e senza alcun sync:
tutte e otto le variabili che il codice chiede rispondono 24 ore su 24, con
numeri plausibili (Bari, 29 set: 17,8 -> 23,6 gradi, umidita' 93 -> 64 %,
umidita' del suolo 0,033-0,039). Con REMOTE_DATA_DIRECTORY l'istanza legge
dal bucket alla richiesta e cachea: `make meteo-sync` serve a togliere la
latenza della prima lettura e a funzionare senza rete, non ad accendere il
servizio. `make meteo-check` dice quali variabili rispondono davvero —
l'healthcheck del container no, risponde anche a volume vuoto.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…esaurito

Due voci nel runbook che sarebbero servite oggi: "alluvione e incendio danno
zero su ogni cella" -- con il conteggio dei degradi, la chiamata a mano che
distingue un tetto da un timeout, e i nodi FWI che calano come sintomo del
fronte che si ritira -- e il 404 di pg_tileserv su una vista appena creata,
che e' il catalogo letto all'avvio e non un errore di nome.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ack dell'host

Una porta pubblicata su 127.0.0.1 non e' raggiungibile da un altro
container: `host.docker.internal` risolve al gateway, non al loopback
dell'host. Misurato, connection refused dal worker.

Il servizio si attacca alla rete `limen_default` (esterna, la crea `make
up`) e api e worker lo chiamano `openmeteo:8080`. La 8085 sul loopback
resta, ma serve all'host -- `make meteo-check` e una curl a mano.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gzileni gzileni changed the title React 19, Mantine 9, e la ragione per cui alluvione e incendio danno zero React 19, Mantine 9, e un'istanza Open-Meteo propria Sep 29, 2026
…sono

Chiude #143.

## Il difetto

Un'integrazione degradata restituisce un risultato neutro, e per un motore
moltiplicativo il neutro è **zero**. Il 29 settembre il tetto Open-Meteo è
saltato e ogni cella d'Italia si è ritrovata con alluvione e incendio a
0,0000: sulla mappa, in fondo alla scala YlOrRd, accanto alla scritta della
classe più tranquilla. È l'unico posto in cui questo sistema sbagliava in
direzione rassicurante.

Il breakdown lo sapeva — `fire_weather: null`, `rain_mm: null` — e il motore
incendio lo dice per esteso nel suo commento: *«a dark cell that declares why
beats a plausible number nobody can source»*. La cella dichiarava, nessuno la
ascoltava.

## La proiezione

Non è ricavabile dal punteggio: una cella genuinamente calma vale zero anche
lei. La differenza la conosce solo il breakdown, ed è lui a rispondere —
`measured()`, la quarta proiezione accanto a `components()`,
`factors_payload()` e `predisposition()`. Incendio: `fire_weather is not
None`. Alluvione: uno dei due segnali grezzi, perché «piove ma non so quanto
è grosso il fiume» è misurata a metà, non non misurata. Frana: `True`, che è
il default, perché un punteggio che poggia sulla suscettibilità del versante
significa qualcosa anche senza pioggia. Aggiungere un pericolo non tocca
nessuno dei lettori.

## Fin dove arriva

`risk_assessments.measured` e `latest_risk.measured` (053), le tre sorgenti
delle tile (054, 055), il rollup per comune con `bool_or` — basta una cella
misurata. NULL è lo storico scritto prima della colonna: «non lo sappiamo»,
che si legge come misurato, perché retro-marcarlo sarebbe inventare al
contrario.

**Esce dall'indice di attenzione.** Contarlo come «sotto soglia» abbassa il
comune due volte, e la seconda è la peggiore: due pericoli misurati e
concomitanti perderebbero l'incremento per colpa di un terzo di cui non
sappiamo niente. Niente di misurato ⇒ `attention` è `None`, mai 0.

Sulla mappa si disegna con lo **stesso grigio** delle celle non valutate:
dicono la stessa cosa a chi guarda, e una sesta tinta renderebbe illeggibile
una scala a cinque classi. Il perché sta nel popup e nella colonna, dove c'è
lo spazio per scriverlo.

## E i numeri si spiegano

Nella colonna convivevano due scale senza dirlo: il punteggio di un pericolo
va da 0 a 1, l'attenzione del comune da 0 a 3 — un 1,37 nudo accanto a celle
da 0,40 non si sa interpretare. Ora il numero grande ha la parola
«attenzione» accanto e un tooltip che dice come si compone; ogni pericolo
mostra il suo punteggio oltre alla classe, con la banda che definisce
«moderato»; la lista delle celle dichiara la sua scala; e un «?» accanto al
titolo apre le tre definizioni per intero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gzileni gzileni changed the title React 19, Mantine 9, e un'istanza Open-Meteo propria React 19, Mantine 9, un'istanza Open-Meteo propria, e «non misurato» che smette di sembrare «tranquillo» Sep 29, 2026
Era nella query e nel DTO, dove è documentata come «la stessa priorità che
il dispacciatore degli alert usa per decidere chi viene prima», ma il
dizionario di risposta non la copiava: usciva il default, 0,000, su ogni
pericolo di ogni comune. Un campo che vale sempre zero è peggio di un campo
assente, perché sembra un dato.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…amento

Tre cose chieste guardando la colonna, più una che si vedeva nello
screenshot senza essere stata chiesta.

## La mappa si sposta

Cliccare un comune apriva la riga e basta: una classifica geografica su cui
non succede niente è una lista di nomi. Ora `mv_comune_risk` porta il suo
centroide fino al DTO — la vista ce l'ha già, quindi non serve una seconda
richiesta — e il clic fa volare la mappa a zoom 11, dove il comune riempie
la vista senza perdere i confini. Anche le celle del dettaglio sono
cliccabili: portano a zoom 13 e selezionano la cella, così il popup si apre
su quella che si stava guardando in lista.

## I punteggi hanno una data

`0,67 Alto · incendio` non diceva se era di adesso o di ieri, e su un rischio
è la differenza fra un'informazione e un numero. `computed_at` arriva dal
dettaglio del comune e si legge come «3 ore fa», non come un ISO.

## L'andamento

Un grafico con le tre serie sulla stessa scala. Sulla stessa **di proposito**,
anche se i tre punteggi hanno calibrazioni diverse: la domanda a cui risponde
non è «quale dei tre è peggio» — per quella c'è il numero di attenzione — ma
«qualcosa sta salendo», e per vedere una salita serve un asse solo.

Segue la **cella peggiore di oggi**, non l'inviluppo del comune ricalcolato
ora per ora. Quella è la domanda più fedele ed è la prima che ho scritto: su
Bardonecchia (134 celle, sette giorni, ventitré partizioni) ci mette **212
secondi**, e non a freddo — rieseguita a cache calda non migliora, perché
tocca davvero troppi dati. Questa ne legge tre celle: 18 s la prima volta su
questo disco, 41 ms dopo. Il prezzo è che la cella peggiore di oggi poteva
non esserlo cinque giorni fa, e il grafico lo dichiara nella sua intestazione;
in compenso concorda con il numero in testata, che è anch'esso la cella
peggiore.

La serie resta **sparsa** come la scrive la #135: niente punto sulle ore in
cui nessuna cella ha lasciato una riga, e i pallini dicono dove una misura
c'è davvero. Un pericolo con un solo punto non diventa una linea: due punti
fanno una tendenza, uno fa un punto, e disegnarlo orizzontale direbbe
«stabile» di un dato che non lo dice. I pericoli non misurati restano fuori.

## E «Stalettì» era «Stalettì»

147 comuni su 7901 avevano il nome doppiamente codificato: i byte UTF-8 di
«ì» letti come due caratteri LATIN1 e ri-codificati. Viene dallo shapefile
ISTAT — che è in ISO-8859-1 — caricato dichiarando UTF-8 nel PostGIS di
GeoServer, a monte di noi, e da lì arrivava intatto fino alla colonna, dove
un cittadino di Cefalù leggeva «Cefalù». La migrazione 056 li ripara e il
seeder fa lo stesso giro quando legge, così un re-seed non li riporta
indietro. Sui nomi già corretti la conversione fallisce e restituisce
l'originale: ripetibile senza danno.

Co-Authored-By: Claude Opus 5 <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.

1 participant