Redesign della colonna: un numero per comune, una lista sola, Mantine - #141
Conversation
La colonna mostra i tre pericoli affiancati, ma per decidere quale comune guardare per primo serviva un numero. Non è la somma dei tre: sommando, 0,40+0,40+0,40 passerebbe davanti a 0,80+0+0 — tre pericoli blandi davanti all'unico comune dove sta per succedere qualcosa — e i tre punteggi non sono nemmeno commensurabili, perché hanno calibrazioni diverse. È il **massimo della priorità già usata per gli alert** — punteggio per uno più l'esposizione — con un incremento quando i pericoli oltre soglia sono più di uno: due pericoli insieme sono una notizia diversa da uno solo, ma non il doppio. La regola sta in `cascades.yaml` come tutte le altre. **L'esposizione era il pezzo mancante.** Il rollup la leggeva da `factors->>'e'`, una chiave che solo il breakdown delle frane ha: per incendio e alluvione valeva zero. Ma «quante persone e quante strade ci sono intorno» è una proprietà del posto, non del pericolo. Ora è materializzata per cella in `cell_static_factors.exposure_norm`, calcolata da `limen calibrate` con la stessa funzione pura del dispacciatore — in SQL non si poteva chiamare, e riscriverla lì avrebbe voluto dire due copie delle stesse soglie libere di divergere. Senza il termine WUI, di proposito: pesa solo per l'incendio, e qui serve una proprietà del posto. Lasciandolo dentro, lo stesso comune avrebbe un'esposizione diversa a seconda del pericolo guardato — la confusione che questo lavoro toglie. **L'effetto misurato in produzione.** Prima la classifica era Arta Terme, Erto e Casso, Verzegnis: comuni di montagna isolati. Ora è Pontebba, Trieste, Spotorno, Zoagli, Genova — stessa classe di rischio, ma con persone e strade sotto. 150.591 celle su 312.550 hanno un'esposizione maggiore di zero. L'ordinamento avviene in due tempi e non è pigrizia: in SQL sulla priorità massima, che è il termine dominante, e in Python per l'incremento multi-pericolo, che ha una soglia di classe e quindi vive nella configurazione. Si prende un margine di tre volte il limite, abbastanza perché un incremento del 15% non possa far entrare in pagina un comune escluso dal taglio. Con le tre skill Mantine installate nel progetto (`.agents/skills/`), che servono al redesign della colonna. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tre cose che la schermata mostrava e che questo cambio toglie. **Le due liste che dicevano la stessa cosa.** «Celle sopra soglia · 72h» era un albero regione → comune → celle, «Comuni · dal peggiore» una lista di comuni: due classifiche degli stessi posti con due unità diverse, una sotto l'altra. Ne resta una, il comune, con le celle come dettaglio che si apre sulla riga. `RegionAccordion` è cancellato, e con lui il terzo livello che chiedeva tre espansioni per arrivare a un nome di posto. **Il selettore dei pericoli lascia la testata.** Se la lettura è «tutti i pericoli insieme», quattro bottoni che cambiano modalità sono il residuo del disegno precedente. Scende sulla mappa, dove filtrare un pericolo alla volta serve davvero, e smette di ridipingere anche la colonna. **Il numero di attenzione** sta accanto al nome, con una barra sotto: è il massimo della priorità pesata per quanto c'è intorno, non la somma dei tre punteggi. Un suggerimento lo spiega a parole, perché «1,35» da solo non dice niente a chi non ha letto la documentazione. **Mantine 8**, non la 9: la 9 pretende React 19 e qui siamo sulla 18, e migrare React è un'altra impresa. Si prendono `TextInput`, `Skeleton`, `Alert`, `Badge`, `Progress`, `Tooltip`, `Collapse`, `Stack`/`Group`, e `useDebouncedValue` al posto del debounce scritto a mano. Il tema eredita i colori del progetto; **la scala del rischio resta fuori dal tema**, in `risk-colors.ts`, perché mappa e colonna devono condividerla byte per byte e un tema che cambia cambierebbe il significato di una cella. Il costo, detto chiaro: il bundle passa a 1,28 MB (380 KB compressi) e il frontend da quattro a sei dipendenze a runtime, in un repository che due settimane fa ha scritto un parser Markdown per non aggiungerne una. In cambio spariscono ~200 righe di CSS e arrivano accessibilità e stati già risolti. Due trappole trovate per strada, entrambe con un messaggio che manda a cercare nel posto sbagliato. `export *` da testing-library ri-esporta `render` e copriva il nostro con provider. E jsdom non implementa `matchMedia` né `ResizeObserver`: senza quei due il provider non monta e ogni componente figlio riporta «MantineProvider was not found», che sembra un errore di albero ed è un polyfill mancante. 113 test verdi, lint e build puliti. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
limen/src/limen/data/repos/comune_risk.py
Lines 109 to 114 in b73c138
For every comune response, hazards is rebuilt without copying the priority value that _PER_COMUNE placed in the JSON. Pydantic then silently supplies ComuneHazard.priority's default of 0.0, so API consumers receive zero for every per-hazard priority even though the same nonzero value was just used to calculate attention; include the rounded priority in this output dictionary.
ℹ️ 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".
| onCellClick={onMapClick} | ||
| selectedCellId={selected?.cellId ?? null} | ||
| /> | ||
| <HazardSelector /> |
There was a problem hiding this comment.
Keep the hazard selector inside the visible map
When two or more hazards are enabled, the selector is now rendered after RiskMap, whose .map-wrap consumes 100% of the map area's height. Since .hazard-selector has no absolute positioning, it enters normal flow below that full-height map and falls outside the viewport, leaving dashboard users unable to switch hazards. Position it as an overlay within .map-area or reserve visible layout space for it.
Useful? React with 👍 / 👎.
| ALTER TABLE cell_static_factors | ||
| ADD COLUMN IF NOT EXISTS exposure_norm double precision; |
There was a problem hiding this comment.
Backfill exposure values during upgrades
On an existing deployment, normal API startup only runs migrations, so this adds exposure_norm as NULL for every existing cell and immediately refreshes mv_comune_risk with priorities that fall back to raw scores. The only backfill is in the separately invoked limen calibrate command, meaning ordinary upgrades silently ignore exposure indefinitely unless an operator knows to run that command; arrange an automatic backfill or an explicit upgrade step before refreshing the materialized view.
Useful? React with 👍 / 👎.
`exposure_rank` non è più la somma del termine E delle celle in allerta, letto dal breakdown delle frane: è il massimo di `exposure_norm` sulle celle del comune, che è una proprietà del posto e vale per tutti e tre i pericoli. Il test fissava il vecchio significato, quindi semina l'esposizione dove ora sta e verifica anche la cosa nuova — che la priorità superi il punteggio nudo, cioè che il peso ci sia davvero. Più la formattazione di `calibrate.py`, che avevo lasciato indietro. Refs #141 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il taglio del blocco CSS del vecchio pannello aveva lasciato il file senza newline finale, e il gancio `end-of-file-fixer` lo segnala. Refs #141 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il commit precedente ne aggiungeva una a un file che ne aveva già una: il gancio ne vuole esattamente una, e stavo correggendo a naso invece di guardare il file. Refs #141 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il redesign concordato, in tre pezzi.
Un numero per comune, pesato su chi c'è intorno
Non la somma dei tre punteggi: sommando, 0,40+0,40+0,40 passerebbe davanti a 0,80+0+0 — tre pericoli blandi davanti all'unico comune dove sta per succedere qualcosa — e i tre numeri non sono nemmeno commensurabili.
È il massimo della priorità già usata per gli alert (punteggio per uno più l'esposizione) con un incremento quando i pericoli oltre soglia sono più di uno. La regola sta in
cascades.yaml.L'esposizione era il pezzo mancante: il rollup la leggeva da
factors->>'e', una chiave che solo il breakdown delle frane ha, quindi per incendio e alluvione valeva zero. Ora è materializzata per cella dalimen calibratecon la stessa funzione pura del dispacciatore — in SQL non si poteva chiamare, e riscriverla lì avrebbe voluto dire due copie delle stesse soglie.Effetto misurato: la classifica passa da Arta Terme, Erto e Casso, Verzegnis (comuni di montagna isolati) a Pontebba, Trieste, Spotorno, Zoagli, Genova — stessa classe di rischio, ma con persone e strade sotto.
Una lista sola
«Celle sopra soglia · 72h» e «Comuni · dal peggiore» erano due classifiche degli stessi posti con due unità diverse, una sotto l'altra. Ne resta una: il comune, con le celle come dettaglio che si apre sulla riga.
Il selettore dei pericoli lascia la testata e scende sulla mappa, dove filtrare serve davvero.
Mantine 8
Non la 9: pretende React 19, qui siamo sulla 18. Si prendono
TextInput,Skeleton,Alert,Badge,Progress,Tooltip,Collapse,Stack/GroupeuseDebouncedValue. Il tema eredita i colori del progetto; la scala del rischio resta fuori dal tema, perché mappa e colonna devono condividerla byte per byte.Il costo: bundle a 1,28 MB (380 KB compressi), frontend da quattro a sei dipendenze. In cambio ~200 righe di CSS in meno e accessibilità già risolta.
Gate
ruff,mypy --strict, 44 test unitari backend (7 nuovi sull'indice di attenzione, che fissano il controesempio contro la somma); frontend 113 test, lint e build puliti.Due trappole documentate nel codice, entrambe con un messaggio che manda a cercare altrove:
export *da testing-library copriva ilrendercon provider, e jsdom senzamatchMedia/ResizeObserverfa dire a ogni figlio «MantineProvider was not found».🤖 Generated with Claude Code