13 KiB
CLAUDE.md — Portale Centraline 37100lab
Lingua
Tutto il progetto deve essere in italiano: commenti nel codice, messaggi all'utente, testo dell'interfaccia, documentazione interna, messaggi di errore, label dei pulsanti, intestazioni CSV.
Fanno eccezione — e restano in inglese — i nomi di variabili, funzioni,
componenti, file, classi CSS, e qualsiasi termine tecnico consolidato (es.
fetch, endpoint, build, download, timestamp).
Panoramica
Portale web per visualizzare le centraline di monitoraggio ambientale di 37100lab e scaricare i dati storici come archivio ZIP contenente CSV.
Stack: SvelteKit + Vite + TypeScript. La lista centraline viene caricata
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,
bun run preview.
Struttura del progetto
download-dati-centraline/
├── CLAUDE.md
├── package.json
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
├── flake.nix
├── static/
│ └── favicon.svg
└── src/
├── app.css # variabili CSS globali, font, stili base
├── app.html # template HTML (carica Poppins da Google Fonts)
├── app.d.ts
├── lib/
│ ├── types.ts # interfacce Station e Measurement
│ ├── api.ts # fetchStations, fetchDay
│ ├── timezone.ts # formatTimestampRome, formatDayParam, getDayRange
│ ├── csv.ts # buildCsv, downloadBlob, buildDayFilename, buildZipFilename
│ ├── download-pool.ts # coda di download con concorrenza limitata
│ ├── groups.json # raggruppamento centraline per area geografica
│ └── 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 esterna
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
Risposta: GeoJSON. Le centraline si trovano in .features[].properties.
Campi rilevanti di ogni .properties:
| Campo | Tipo | Note |
|---|---|---|
title |
string | Nome della centralina |
pk |
string | ID numerico, arriva come stringa |
created_at |
ISO 8601 UTC | Data/ora di creazione |
ended_at |
string o null | Data/ora di fine; null se ancora attiva |
Il campo pk va convertito a numero intero.
GET /api/get_measurements/{id}?day=YYYY-M-D — misurazioni giornaliere
{id}: ID numerico della centralinaday: data inEurope/Rome, formatoYYYY-M-Dsenza zero-padding- ✅
2026-5-6 - ❌
2026-05-06
- ✅
La risposta contiene una chiave misure con un array di oggetti, oppure un
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 |
|---|---|---|
temp |
number | Temperatura (°C) |
umid |
number | Umidità relativa (%) |
pm10 |
number | PM10 (µg/m³) |
time |
ISO UTC | Timestamp misurazione |
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:
- Converte
dateinYYYY-M-Dsenza zero-padding prima di chiamare il backend. - Sostituisce
NaN/Infinity/-infconnullnella risposta grezza. - Filtra le misurazioni con
timenon valido o oggettinull. - Aggiunge header
Cache-Control: 1 settimana per giorni precedenti a ieri, 60 secondi per ieri e oggi. Indev, usano-store.
Il client chiama questa route passando date in formato YYYY-MM-DD; la route
si occupa della conversione al formato del backend.
Formato archivio ZIP
Il download produce un file .zip contenente:
-
Un CSV per ogni giorno con misurazioni, senza colonna Data:
"Ora","Temperatura","Umidità","PM10" "22:00:53",13.45,89.22,16.3Nome 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.3Nome 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 |
|---|---|
| Encoding | UTF-8 con BOM () — necessario per Excel italiano |
| Separatore | virgola |
| Intestazioni | tra doppi apici, prima riga |
| 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) |
| Timestamp | convertito da UTC a Europe/Rome (gestisce ora legale) |
Libreria di compressione: jszip (unica dipendenza runtime).
Moduli
lib/types.ts
Interfacce TypeScript condivise: Station e Measurement.
lib/api.ts
-
fetchStations(fetch?): carica la lista centraline dal backend esterno. Accetta ilfetchdi SvelteKit per il SSR. Lancia eccezione in caso di errore (gestita da+page.server.ts). Ordina perpkdecrescente. -
fetchDay(id, day, signal): scarica le misurazioni di un giorno chiamando la route interna/api/get_station_data?id=…&date=YYYY-MM-DD. Accetta unAbortSignal; ritenta fino a 3 volte in caso di errore; restituisce sempre[]in caso di errore o abort (non lancia mai eccezioni).
lib/timezone.ts
Gestisce date e timezone senza librerie esterne.
-
formatTimestampRome(isoString): converte un timestamp ISO UTC nel formatoDD/MM/YYYY HH:MM:SS.s. Usa un offset cache (per ora UTC) per evitare chiamate ripetute aIntl.DateTimeFormat. Il decimale dei secondi è troncato (non arrotondato):706ms → .7. -
formatDayParam(date): restituisce la data locale Europe/Rome nel formatoYYYY-M-Dsenza zero-padding, tramiteIntl.DateTimeFormatconformatToParts. -
getDayRange(startIsoUtc, endIsoUtc): restituisce un array diDate, uno per ogni giorno dal giorno distartIsoUtcal giorno diendIsoUtc(estremi inclusi) inEurope/Rome. SeendIsoUtcènull, il limite superiore è oggi. Usa mezzogiorno UTC (T12:00:00Z) come ancora giornaliera per non attraversare mai un cambio di data locale.
lib/csv.ts
-
buildCsv(measurements, includeDate?): costruisce la stringa CSV con BOM. SeincludeDateètrue(default), include la colonna "Data"; altrimenti solo "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): restituisceYYYY-MM-DD.csvinEurope/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
routes/+page.server.ts
Funzione load di SvelteKit: chiama fetchStations lato server al page load.
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
Raggruppa le centraline in sezioni usando groups.json. All'interno di ogni
sezione, ordina: attive prima delle terminate, poi per created_at decrescente.
Le centraline non in nessun gruppo vanno nella sezione "Altre". Sezioni vuote
vengono omesse.
StationRow.svelte
Card di una singola centralina. Contiene tutta la logica di download:
- Calcola il range di giorni con
getDayRange(limite superiore:ended_ato oggi per le attive). - Scarica tutti i giorni in parallelo tramite
enqueuedal pool (max 3 richieste simultanee) conPromise.all. - La cancellazione avviene tramite
AbortController:controller.abort()interrompe le fetch in corso;fetchDayintercettaAbortErrore ritorna[]. - Tiene traccia del progresso con i contatori
currentDay/totalDays(incrementati al completamento di ogni giorno). - Al completamento di tutte le fetch (non cancellate), genera lo ZIP con
jszipe lo scarica. - Il pulsante di download mostra uno spinner durante il fetch; al hover diventa un'icona ✕ per annullare. Al completamento mostra un segno di spunta verde per 2 secondi.
- La barra di progresso (3px, fondo card) si colora di verde al completamento e di rosso se annullata.
- In
onDestroy, chiamacontroller?.abort()per pulire fetch pendenti se il componente viene smontato.
Il tooltip sulla data di fine centralina è implementato come Svelte action
(floatingTooltip) che crea/rimuove un div nel document.body — necessario
perché la card ha overflow: hidden e non può contenere il tooltip.
Edge case e comportamenti attesi
| Situazione | Comportamento |
|---|---|
Giorno senza misurazioni (misure: []) |
Ignorare, il giorno non contribuisce allo ZIP |
| Errore HTTP su un singolo giorno | fetchDay restituisce [] dopo 3 tentativi, ignorare e continuare |
| Download annullato dall'utente | AbortController interrompe le fetch, nessun file scaricato |
| 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 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 |
| 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
Font: Poppins (Google Fonts, pesi 400/500/600/700).
Palette (variabili CSS in app.css):
| 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}).