Updated CLAUDE.md to reflect current codebase
This commit is contained in:
@@ -14,48 +14,67 @@ componenti, file, classi CSS, e qualsiasi termine tecnico consolidato (es.
|
|||||||
|
|
||||||
## Panoramica
|
## Panoramica
|
||||||
|
|
||||||
Portale web **statico** (solo frontend, nessun backend) per visualizzare le
|
Portale web per visualizzare le centraline di monitoraggio ambientale di
|
||||||
centraline di monitoraggio ambientale di 37100lab e scaricare i dati storici in
|
37100lab e scaricare i dati storici come archivio ZIP contenente CSV.
|
||||||
formato CSV.
|
|
||||||
|
|
||||||
**Stack**: Svelte + Vite. Output finale: cartella `dist/` da servire con
|
**Stack**: SvelteKit + Vite + TypeScript. La lista centraline viene caricata
|
||||||
qualsiasi web server statico.
|
lato server al page load (`+page.server.ts`); le richieste di misurazioni
|
||||||
|
vengono instradate attraverso una route API interna (`/api/get_station_data`)
|
||||||
|
che funge da proxy verso il backend esterno, aggiungendo sanitizzazione e
|
||||||
|
cache HTTP.
|
||||||
|
|
||||||
---
|
**Ambiente di sviluppo**: Nix flake + Bun. Entrare nell'ambiente con
|
||||||
|
`nix develop`, poi usare i comandi `bun run dev`, `bun run build`,
|
||||||
## Setup iniziale
|
`bun run preview`.
|
||||||
|
|
||||||
Usare il template Svelte ufficiale di Vite. I comandi di sviluppo standard sono
|
|
||||||
`dev`, `build` e `preview`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Struttura del progetto
|
## Struttura del progetto
|
||||||
|
|
||||||
```
|
```
|
||||||
portale-centraline/
|
download-dati-centraline/
|
||||||
├── CLAUDE.md
|
├── CLAUDE.md
|
||||||
├── package.json
|
├── package.json
|
||||||
├── vite.config.js
|
├── svelte.config.js
|
||||||
├── index.html
|
├── vite.config.ts
|
||||||
|
├── tsconfig.json
|
||||||
|
├── flake.nix
|
||||||
|
├── static/
|
||||||
|
│ └── favicon.svg
|
||||||
└── src/
|
└── src/
|
||||||
├── App.svelte
|
├── app.css # variabili CSS globali, font, stili base
|
||||||
|
├── app.html # template HTML (carica Poppins da Google Fonts)
|
||||||
|
├── app.d.ts
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── api.js # fetch verso l'API
|
│ ├── types.ts # interfacce Station e Measurement
|
||||||
│ ├── timezone.js # conversione UTC → Europe/Rome, iterazione giorni
|
│ ├── api.ts # fetchStations, fetchDay
|
||||||
│ └── csv.js # costruzione CSV e download
|
│ ├── timezone.ts # formatTimestampRome, formatDayParam, getDayRange
|
||||||
└── components/
|
│ ├── csv.ts # buildCsv, downloadBlob, buildDayFilename, buildZipFilename
|
||||||
├── StationList.svelte
|
│ ├── download-pool.ts # coda di download con concorrenza limitata
|
||||||
├── StationRow.svelte
|
│ ├── groups.json # raggruppamento centraline per area geografica
|
||||||
└── DownloadProgress.svelte
|
│ └── components/
|
||||||
|
│ ├── StationList.svelte
|
||||||
|
│ └── StationRow.svelte
|
||||||
|
└── routes/
|
||||||
|
├── +layout.svelte # importa app.css
|
||||||
|
├── +page.svelte # pagina principale
|
||||||
|
├── +page.server.ts # carica la lista centraline lato server
|
||||||
|
└── api/
|
||||||
|
└── get_station_data/
|
||||||
|
└── +server.ts # proxy verso il backend esterno
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API
|
## API esterna
|
||||||
|
|
||||||
**Base URL**: `http://37100lab.it:8101`
|
**Base URL**: `http://37100lab.it:8101`
|
||||||
|
|
||||||
|
Il frontend non chiama mai direttamente il backend esterno per le misurazioni:
|
||||||
|
usa la route interna `/api/get_station_data` che fa da proxy (vedi sotto).
|
||||||
|
Solo `fetchStations` chiama direttamente il backend (dalla funzione `load`
|
||||||
|
di SvelteKit, lato server).
|
||||||
|
|
||||||
### GET `/api/campagne_map_data` — lista centraline
|
### GET `/api/campagne_map_data` — lista centraline
|
||||||
|
|
||||||
Risposta: GeoJSON. Le centraline si trovano in `.features[].properties`.
|
Risposta: GeoJSON. Le centraline si trovano in `.features[].properties`.
|
||||||
@@ -69,8 +88,7 @@ Campi rilevanti di ogni `.properties`:
|
|||||||
| `created_at` | ISO 8601 UTC | Data/ora di creazione |
|
| `created_at` | ISO 8601 UTC | Data/ora di creazione |
|
||||||
| `ended_at` | string o null | Data/ora di fine; null se ancora attiva |
|
| `ended_at` | string o null | Data/ora di fine; null se ancora attiva |
|
||||||
|
|
||||||
Il campo `pk` va convertito a numero intero. Le centraline vanno ordinate per ID
|
Il campo `pk` va convertito a numero intero.
|
||||||
decrescente.
|
|
||||||
|
|
||||||
### GET `/api/get_measurements/{id}?day=YYYY-M-D` — misurazioni giornaliere
|
### GET `/api/get_measurements/{id}?day=YYYY-M-D` — misurazioni giornaliere
|
||||||
|
|
||||||
@@ -79,8 +97,9 @@ decrescente.
|
|||||||
- ✅ `2026-5-6`
|
- ✅ `2026-5-6`
|
||||||
- ❌ `2026-05-06`
|
- ❌ `2026-05-06`
|
||||||
|
|
||||||
La risposta contiene una chiave `misure` con un array di oggetti. Ogni oggetto
|
La risposta contiene una chiave `misure` con un array di oggetti, oppure un
|
||||||
ha:
|
array vuoto. Il backend può emettere `NaN`/`Infinity` come valori JSON non
|
||||||
|
standard — il proxy li sostituisce con `null` prima di parsare.
|
||||||
|
|
||||||
| Campo | Tipo | Descrizione |
|
| Campo | Tipo | Descrizione |
|
||||||
|--------|---------|-----------------------|
|
|--------|---------|-----------------------|
|
||||||
@@ -89,135 +108,209 @@ ha:
|
|||||||
| `pm10` | number | PM10 (µg/m³) |
|
| `pm10` | number | PM10 (µg/m³) |
|
||||||
| `time` | ISO UTC | Timestamp misurazione |
|
| `time` | ISO UTC | Timestamp misurazione |
|
||||||
|
|
||||||
`misure` può essere un array vuoto se il giorno non ha dati.
|
---
|
||||||
|
|
||||||
|
## Route API interna: `/api/get_station_data`
|
||||||
|
|
||||||
|
`src/routes/api/get_station_data/+server.ts`
|
||||||
|
|
||||||
|
Parametri query: `id` (numero) e `date` (formato `YYYY-MM-DD` con zero-padding).
|
||||||
|
|
||||||
|
Comportamento:
|
||||||
|
1. Converte `date` in `YYYY-M-D` senza zero-padding prima di chiamare il backend.
|
||||||
|
2. Sostituisce `NaN`/`Infinity`/`-inf` con `null` nella risposta grezza.
|
||||||
|
3. Filtra le misurazioni con `time` non valido o oggetti `null`.
|
||||||
|
4. Aggiunge header `Cache-Control`: **1 settimana** per giorni precedenti a ieri,
|
||||||
|
**60 secondi** per ieri e oggi. In `dev`, usa `no-store`.
|
||||||
|
|
||||||
|
Il client chiama questa route passando `date` in formato `YYYY-MM-DD`; la route
|
||||||
|
si occupa della conversione al formato del backend.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Formato CSV
|
## Formato archivio ZIP
|
||||||
|
|
||||||
|
Il download produce un file `.zip` contenente:
|
||||||
|
|
||||||
|
- **Un CSV per ogni giorno** con misurazioni, senza colonna Data:
|
||||||
```
|
```
|
||||||
"Data","Temperatura","Umidità","PM10"
|
"Ora","Temperatura","Umidità","PM10"
|
||||||
"05/05/2026 22:00:53.7",13.45,89.22,16.3
|
"22:00:53",13.45,89.22,16.3
|
||||||
"05/05/2026 22:01:53.9",13.45,89.02,16.1
|
|
||||||
"05/05/2026 22:02:54.1",13.47,88.88,15.5
|
|
||||||
```
|
```
|
||||||
|
Nome file: `YYYY-MM-DD.csv`
|
||||||
|
|
||||||
|
- **Un CSV combinato** con tutte le misurazioni, con colonna Data:
|
||||||
|
```
|
||||||
|
"Data","Ora","Temperatura","Umidità","PM10"
|
||||||
|
"05/05/2026","22:00:53",13.45,89.22,16.3
|
||||||
|
```
|
||||||
|
Nome file: `{NomeCentralina}_(id).csv`
|
||||||
|
|
||||||
|
Nome del file ZIP: `{NomeCentralina}_(id).zip`
|
||||||
|
Esempio: `FabLab_Mantova_(178).zip`
|
||||||
|
Sanitizzazione: spazi → `_`, caratteri non alfanumerici (eccetto `_`) rimossi.
|
||||||
|
|
||||||
|
### Caratteristiche CSV
|
||||||
|
|
||||||
| Caratteristica | Valore |
|
| Caratteristica | Valore |
|
||||||
|-------------------|--------------------------------------------------------------|
|
|-------------------|--------------------------------------------------------------|
|
||||||
| Encoding | **UTF-8 con BOM** (`\uFEFF`) — necessario per Excel italiano |
|
| Encoding | **UTF-8 con BOM** (``) — necessario per Excel italiano |
|
||||||
| Separatore | virgola |
|
| Separatore | virgola |
|
||||||
| Intestazioni | tra doppi apici, prima riga |
|
| Intestazioni | tra doppi apici, prima riga |
|
||||||
| Colonna Data | stringa tra doppi apici, formato `DD/MM/YYYY HH:MM:SS.s` |
|
| Colonna Data | stringa tra doppi apici, formato `DD/MM/YYYY` |
|
||||||
|
| Colonna Ora | stringa tra doppi apici, formato `HH:MM:SS` (senza decimali) |
|
||||||
| Colonne numeriche | senza apici, valori originali (non arrotondare) |
|
| Colonne numeriche | senza apici, valori originali (non arrotondare) |
|
||||||
| Timestamp | convertito da UTC a **Europe/Rome** (gestisce ora legale) |
|
| Timestamp | convertito da UTC a **Europe/Rome** (gestisce ora legale) |
|
||||||
|
|
||||||
**Nome file**: `{NomeCentralina}_({id})_{YYYY-MM-DD}.csv` Esempio:
|
Libreria di compressione: `jszip` (unica dipendenza runtime).
|
||||||
`FabLab_Mantova_(178)_2026-05-07.csv` Sostituire spazi con `_`, rimuovere
|
|
||||||
caratteri non alfanumerici eccetto `_`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Moduli
|
## Moduli
|
||||||
|
|
||||||
### `lib/api.js`
|
### `lib/types.ts`
|
||||||
|
|
||||||
Contiene due funzioni: una per caricare la lista delle centraline e una per
|
Interfacce TypeScript condivise: `Station` e `Measurement`.
|
||||||
caricare le misurazioni di un singolo giorno. La seconda non deve mai lanciare
|
|
||||||
eccezioni — in caso di errore HTTP o di rete restituisce un array vuoto, così da
|
|
||||||
non interrompere il loop di download.
|
|
||||||
|
|
||||||
### `lib/timezone.js`
|
### `lib/api.ts`
|
||||||
|
|
||||||
Gestisce tutto ciò che riguarda date e timezone. Non usare librerie esterne:
|
- **`fetchStations(fetch?)`**: carica la lista centraline dal backend esterno.
|
||||||
`Intl.DateTimeFormat` con `timeZone: 'Europe/Rome'` è sufficiente e gestisce
|
Accetta il `fetch` di SvelteKit per il SSR. Lancia eccezione in caso di
|
||||||
automaticamente il cambio ora legale (CET/CEST).
|
errore (gestita da `+page.server.ts`). Ordina per `pk` decrescente.
|
||||||
|
|
||||||
Contiene tre funzioni:
|
- **`fetchDay(id, day, signal)`**: scarica le misurazioni di un giorno
|
||||||
|
chiamando la route interna `/api/get_station_data?id=…&date=YYYY-MM-DD`.
|
||||||
|
Accetta un `AbortSignal`; ritenta fino a 3 volte in caso di errore; restituisce
|
||||||
|
sempre `[]` in caso di errore o abort (non lancia mai eccezioni).
|
||||||
|
|
||||||
- **`formatTimestampRome(isoString)`**: converte un timestamp ISO UTC nella
|
### `lib/timezone.ts`
|
||||||
stringa formattata per il CSV (`DD/MM/YYYY HH:MM:SS.s`). I millisecondi
|
|
||||||
vengono troncati al primo decimale con `Math.floor`, non arrotondati: `706ms →
|
Gestisce date e timezone senza librerie esterne.
|
||||||
.7`. I millisecondi sono indipendenti dal timezone e si leggono direttamente
|
|
||||||
dall'oggetto `Date`.
|
- **`formatTimestampRome(isoString)`**: converte un timestamp ISO UTC nel formato
|
||||||
|
`DD/MM/YYYY HH:MM:SS.s`. Usa un offset cache (per ora UTC) per evitare chiamate
|
||||||
|
ripetute a `Intl.DateTimeFormat`. Il decimale dei secondi è troncato (non
|
||||||
|
arrotondato): `706ms → .7`.
|
||||||
|
|
||||||
- **`formatDayParam(date)`**: restituisce la data locale Europe/Rome nel formato
|
- **`formatDayParam(date)`**: restituisce la data locale Europe/Rome nel formato
|
||||||
`YYYY-M-D` senza zero-padding, da usare come parametro `day` nelle chiamate
|
`YYYY-M-D` senza zero-padding, tramite `Intl.DateTimeFormat` con
|
||||||
API. Usare `Intl.DateTimeFormat` con `month: 'numeric'` e `day: 'numeric'` in
|
`formatToParts`.
|
||||||
`formatToParts` — in questo modo i valori non hanno zero-padding.
|
|
||||||
|
|
||||||
- **`getDayRange(startIsoUtc, endIsoUtc)`**: restituisce un array di oggetti
|
- **`getDayRange(startIsoUtc, endIsoUtc)`**: restituisce un array di `Date`,
|
||||||
`Date`, uno per ogni giorno dal giorno di `startIsoUtc` al giorno di
|
uno per ogni giorno dal giorno di `startIsoUtc` al giorno di `endIsoUtc`
|
||||||
`endIsoUtc` (estremi inclusi), calcolati in `Europe/Rome`. Se `endIsoUtc` è
|
(estremi inclusi) in `Europe/Rome`. Se `endIsoUtc` è `null`, il limite
|
||||||
`null`, il limite superiore è oggi. Usare **mezzogiorno UTC** (`T12:00:00Z`)
|
superiore è oggi. Usa **mezzogiorno UTC** (`T12:00:00Z`) come ancora giornaliera
|
||||||
come ancora per ogni cursore giornaliero: mezzogiorno UTC corrisponde alle
|
per non attraversare mai un cambio di data locale.
|
||||||
13:00 o 14:00 a Roma, quindi non attraversa mai un cambio di data locale
|
|
||||||
neanche al cambio ora legale.
|
|
||||||
|
|
||||||
### `lib/csv.js`
|
### `lib/csv.ts`
|
||||||
|
|
||||||
Contiene tre funzioni: una per costruire la stringa CSV da un array di
|
- **`buildCsv(measurements, includeDate?)`**: costruisce la stringa CSV con BOM.
|
||||||
misurazioni (con BOM), una per triggerare il download nel browser, e una per
|
Se `includeDate` è `true` (default), include la colonna "Data"; altrimenti solo
|
||||||
costruire il nome del file.
|
"Ora". L'ora è troncata ai secondi (niente decimali).
|
||||||
|
|
||||||
|
- **`downloadBlob(blob, filename)`**: crea un URL oggetto, simula il click su un
|
||||||
|
link `<a download>`, poi revoca l'URL.
|
||||||
|
|
||||||
|
- **`buildDayFilename(date)`**: restituisce `YYYY-MM-DD.csv` in `Europe/Rome`.
|
||||||
|
|
||||||
|
- **`buildZipFilename(title, id)`**: sanitizza il nome e restituisce
|
||||||
|
`{NomeSanitizzato}_(id).zip`.
|
||||||
|
|
||||||
|
### `lib/download-pool.ts`
|
||||||
|
|
||||||
|
Pool di concorrenza per le richieste HTTP di download. Costante
|
||||||
|
`DOWNLOAD_WORKERS = 3` (richieste parallele massime). La funzione `enqueue(fn)`
|
||||||
|
accoda un'operazione e restituisce una `Promise` che si risolve al termine.
|
||||||
|
Gestisce internamente la coda FIFO e il contatore di worker attivi.
|
||||||
|
|
||||||
|
### `lib/groups.json`
|
||||||
|
|
||||||
|
Array di oggetti `{ name, ids[] }` che mappa nomi di aree geografiche a set di
|
||||||
|
ID centralina. Usato da `StationList` per raggruppare le card. Le centraline non
|
||||||
|
presenti in nessun gruppo finiscono nella sezione "Altre".
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Componenti
|
## Componenti
|
||||||
|
|
||||||
### `App.svelte`
|
### `routes/+page.server.ts`
|
||||||
|
|
||||||
Componente radice. Al montaggio carica la lista delle centraline e gestisce i
|
Funzione `load` di SvelteKit: chiama `fetchStations` lato server al page load.
|
||||||
tre stati: caricamento, errore, lista pronta.
|
In caso di errore restituisce `{ stations: [], loadError: '…' }`.
|
||||||
|
|
||||||
|
### `routes/+page.svelte`
|
||||||
|
|
||||||
|
Pagina principale. Riceve `data` da `+page.server.ts`. Mostra il messaggio di
|
||||||
|
errore se presente, altrimenti renderizza `StationList`. Contiene header e footer.
|
||||||
|
|
||||||
### `StationList.svelte`
|
### `StationList.svelte`
|
||||||
|
|
||||||
Riceve l'array di centraline e le mostra in una tabella con colonne: ID, nome,
|
Raggruppa le centraline in sezioni usando `groups.json`. All'interno di ogni
|
||||||
data di creazione (formattata in `Europe/Rome`), stato (attiva/terminata),
|
sezione, ordina: attive prima delle terminate, poi per `created_at` decrescente.
|
||||||
pulsante download.
|
Le centraline non in nessun gruppo vanno nella sezione "Altre". Sezioni vuote
|
||||||
|
vengono omesse.
|
||||||
|
|
||||||
### `StationRow.svelte`
|
### `StationRow.svelte`
|
||||||
|
|
||||||
Gestisce la riga di una singola centralina e contiene tutta la logica di
|
Card di una singola centralina. Contiene tutta la logica di download:
|
||||||
download:
|
|
||||||
|
|
||||||
1. Calcola il range di giorni con `getDayRange`. Le centraline **attive**
|
1. Calcola il range di giorni con `getDayRange` (limite superiore: `ended_at` o
|
||||||
(`ended_at: null`) usano oggi come limite superiore; le centraline
|
oggi per le attive).
|
||||||
**terminate** (`ended_at` valorizzato) usano `ended_at` come limite
|
2. Scarica tutti i giorni **in parallelo** tramite `enqueue` dal pool
|
||||||
superiore.
|
(max 3 richieste simultanee) con `Promise.all`.
|
||||||
2. Itera i giorni **sequenzialmente** (non in parallelo) per non sovraccaricare
|
3. La cancellazione avviene tramite `AbortController`: `controller.abort()`
|
||||||
l'API.
|
interrompe le fetch in corso; `fetchDay` intercetta `AbortError` e ritorna `[]`.
|
||||||
3. Per ogni giorno chiama `fetchDay` e accumula le misurazioni.
|
4. Tiene traccia del progresso con i contatori `currentDay` / `totalDays`
|
||||||
4. Aggiorna il contatore di progresso ad ogni giorno completato.
|
(incrementati al completamento di ogni giorno).
|
||||||
5. Supporta la cancellazione: controlla un flag `cancelled` ad ogni iterazione;
|
5. Al completamento di tutte le fetch (non cancellate), genera lo ZIP con `jszip`
|
||||||
se attivo, interrompe il loop senza scaricare nulla.
|
e lo scarica.
|
||||||
6. Al termine, se ci sono misurazioni e il download non è stato annullato,
|
6. Il pulsante di download mostra uno spinner durante il fetch; al hover diventa
|
||||||
genera e scarica il CSV.
|
un'icona ✕ per annullare. Al completamento mostra un segno di spunta verde per
|
||||||
|
2 secondi.
|
||||||
|
7. La barra di progresso (3px, fondo card) si colora di verde al completamento e
|
||||||
|
di rosso se annullata.
|
||||||
|
8. In `onDestroy`, chiama `controller?.abort()` per pulire fetch pendenti se il
|
||||||
|
componente viene smontato.
|
||||||
|
|
||||||
### `DownloadProgress.svelte`
|
Il tooltip sulla data di fine centralina è implementato come Svelte action
|
||||||
|
(`floatingTooltip`) che crea/rimuove un `div` nel `document.body` — necessario
|
||||||
Mostra il progresso durante il download: giorno corrente, totale giorni e
|
perché la card ha `overflow: hidden` e non può contenere il tooltip.
|
||||||
percentuale. Ha un pulsante "Annulla" che emette un evento verso `StationRow`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Edge case e comportamenti attesi
|
## Edge case e comportamenti attesi
|
||||||
|
|
||||||
| Situazione | Comportamento |
|
| Situazione | Comportamento |
|
||||||
|-----------------------------------------------|-----------------------------------------------------------------------|
|
|-----------------------------------------------|----------------------------------------------------------------------------|
|
||||||
| Giorno senza misurazioni (`misure: []`) | Ignorare, continuare con il giorno successivo |
|
| Giorno senza misurazioni (`misure: []`) | Ignorare, il giorno non contribuisce allo ZIP |
|
||||||
| Errore HTTP su un singolo giorno | `fetchDay` restituisce `[]`, ignorare e continuare |
|
| Errore HTTP su un singolo giorno | `fetchDay` restituisce `[]` dopo 3 tentativi, ignorare e continuare |
|
||||||
| Download annullato dall'utente | Il loop si interrompe al controllo del flag, nessun file scaricato |
|
| Download annullato dall'utente | `AbortController` interrompe le fetch, nessun file scaricato |
|
||||||
| Nessuna misurazione dopo tutti i giorni | Non generare il CSV, nessun download |
|
| Nessuna misurazione dopo tutti i giorni | Non generare lo ZIP, nessun download |
|
||||||
| Centralina attiva (`ended_at: null`) | Range: da `created_at` a **oggi** in Europe/Rome |
|
| Centralina attiva (`ended_at: null`) | Range: da `created_at` a **oggi** in Europe/Rome |
|
||||||
| Centralina terminata (`ended_at` valorizzato) | Range: da `created_at` a **`ended_at`** in Europe/Rome |
|
| Centralina terminata (`ended_at` valorizzato) | Range: da `created_at` a **`ended_at`** in Europe/Rome |
|
||||||
| Nome centralina con caratteri speciali | Sanitizzare per il nome file: spazi → `_`, resto rimosso |
|
| Nome centralina con caratteri speciali | Sanitizzare per il nome file: spazi → `_`, resto rimosso |
|
||||||
| Errore nel caricamento della lista | `App.svelte` mostra un messaggio di errore all'utente |
|
| Errore nel caricamento della lista | `+page.svelte` mostra un messaggio di errore |
|
||||||
|
| Backend emette `NaN`/`Infinity` | Il proxy li sostituisce con `null` con regex prima del parse |
|
||||||
|
| Timestamp non valido nella risposta | `+server.ts` filtra le misurazioni con `time` non parsabile |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
Il portale riguarda dati ambientali e scientifici raccolti sul campo da stazioni
|
Font: **Poppins** (Google Fonts, pesi 400/500/600/700).
|
||||||
fisiche. L'estetica deve riflettere questo contesto: strumentale, precisa,
|
|
||||||
leggibile. Scegliere una direzione stilistica netta — non generica. Evitare
|
Palette (variabili CSS in `app.css`):
|
||||||
palette pastello, font di sistema e layout cookie-cutter. Curare tipografia,
|
|
||||||
spaziatura e la gerarchia visiva della tabella.
|
| Variabile | Valore | Uso |
|
||||||
|
|---------------|-----------|----------------------------------|
|
||||||
|
| `--bg` | `#f0f2f5` | sfondo pagina |
|
||||||
|
| `--surface` | `#ffffff` | sfondo card |
|
||||||
|
| `--border` | `#dde1e7` | bordi |
|
||||||
|
| `--text` | `#374151` | testo normale |
|
||||||
|
| `--text-dim` | `#9ca3af` | testo secondario |
|
||||||
|
| `--text-hi` | `#111827` | testo in evidenza |
|
||||||
|
| `--accent` | `#2563eb` | blu (link, attivo, progresso) |
|
||||||
|
|
||||||
|
Il layout usa card arrotondate (`border-radius: 14px`) al posto di una tabella.
|
||||||
|
Le centraline attive sono in evidenza rispetto alle terminate. Il nome è un link
|
||||||
|
alla pagina della centralina sul backend (`/campagna/{pk}`).
|
||||||
|
|||||||
Reference in New Issue
Block a user