# 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 **statico** (solo frontend, nessun backend) per visualizzare le centraline di monitoraggio ambientale di 37100lab e scaricare i dati storici in formato CSV. **Stack**: Svelte + Vite. Output finale: cartella `dist/` da servire con qualsiasi web server statico. --- ## Setup iniziale Usare il template Svelte ufficiale di Vite. I comandi di sviluppo standard sono `dev`, `build` e `preview`. --- ## Struttura del progetto ``` portale-centraline/ ├── CLAUDE.md ├── package.json ├── vite.config.js ├── index.html └── src/ ├── App.svelte ├── lib/ │ ├── api.js # fetch verso l'API │ ├── timezone.js # conversione UTC → Europe/Rome, iterazione giorni │ └── csv.js # costruzione CSV e download └── components/ ├── StationList.svelte ├── StationRow.svelte └── DownloadProgress.svelte ``` --- ## API **Base URL**: `http://37100lab.it:8101` ### 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. Le centraline vanno ordinate per ID decrescente. ### GET `/api/get_measurements/{id}?day=YYYY-M-D` — misurazioni giornaliere - `{id}`: ID numerico della centralina - `day`: data in **`Europe/Rome`**, formato **`YYYY-M-D` senza zero-padding** - ✅ `2026-5-6` - ❌ `2026-05-06` La risposta contiene una chiave `misure` con un array di oggetti. Ogni oggetto ha: | Campo | Tipo | Descrizione | |--------|---------|-----------------------| | `temp` | number | Temperatura (°C) | | `umid` | number | Umidità relativa (%) | | `pm10` | number | PM10 (µg/m³) | | `time` | ISO UTC | Timestamp misurazione | `misure` può essere un array vuoto se il giorno non ha dati. --- ## Formato CSV ``` "Data","Temperatura","Umidità","PM10" "05/05/2026 22:00:53.7",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 ``` | Caratteristica | Valore | |-------------------|--------------------------------------------------------------| | Encoding | **UTF-8 con BOM** (`\uFEFF`) — necessario per Excel italiano | | Separatore | virgola | | Intestazioni | tra doppi apici, prima riga | | Colonna Data | stringa tra doppi apici, formato `DD/MM/YYYY HH:MM:SS.s` | | Colonne numeriche | senza apici, valori originali (non arrotondare) | | Timestamp | convertito da UTC a **Europe/Rome** (gestisce ora legale) | **Nome file**: `{NomeCentralina}_({id})_{YYYY-MM-DD}.csv` Esempio: `FabLab_Mantova_(178)_2026-05-07.csv` Sostituire spazi con `_`, rimuovere caratteri non alfanumerici eccetto `_`. --- ## Moduli ### `lib/api.js` Contiene due funzioni: una per caricare la lista delle centraline e una per 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` Gestisce tutto ciò che riguarda date e timezone. Non usare librerie esterne: `Intl.DateTimeFormat` con `timeZone: 'Europe/Rome'` è sufficiente e gestisce automaticamente il cambio ora legale (CET/CEST). Contiene tre funzioni: - **`formatTimestampRome(isoString)`**: converte un timestamp ISO UTC nella 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 → .7`. I millisecondi sono indipendenti dal timezone e si leggono direttamente dall'oggetto `Date`. - **`formatDayParam(date)`**: restituisce la data locale Europe/Rome nel formato `YYYY-M-D` senza zero-padding, da usare come parametro `day` nelle chiamate API. Usare `Intl.DateTimeFormat` con `month: 'numeric'` e `day: 'numeric'` in `formatToParts` — in questo modo i valori non hanno zero-padding. - **`getDayRange(startIsoUtc, endIsoUtc)`**: restituisce un array di oggetti `Date`, uno per ogni giorno dal giorno di `startIsoUtc` al giorno di `endIsoUtc` (estremi inclusi), calcolati in `Europe/Rome`. Se `endIsoUtc` è `null`, il limite superiore è oggi. Usare **mezzogiorno UTC** (`T12:00:00Z`) come ancora per ogni cursore giornaliero: mezzogiorno UTC corrisponde alle 13:00 o 14:00 a Roma, quindi non attraversa mai un cambio di data locale neanche al cambio ora legale. ### `lib/csv.js` Contiene tre funzioni: una per costruire la stringa CSV da un array di misurazioni (con BOM), una per triggerare il download nel browser, e una per costruire il nome del file. --- ## Componenti ### `App.svelte` Componente radice. Al montaggio carica la lista delle centraline e gestisce i tre stati: caricamento, errore, lista pronta. ### `StationList.svelte` Riceve l'array di centraline e le mostra in una tabella con colonne: ID, nome, data di creazione (formattata in `Europe/Rome`), stato (attiva/terminata), pulsante download. ### `StationRow.svelte` Gestisce la riga di una singola centralina e contiene tutta la logica di download: 1. Calcola il range di giorni con `getDayRange`. Le centraline **attive** (`ended_at: null`) usano oggi come limite superiore; le centraline **terminate** (`ended_at` valorizzato) usano `ended_at` come limite superiore. 2. Itera i giorni **sequenzialmente** (non in parallelo) per non sovraccaricare l'API. 3. Per ogni giorno chiama `fetchDay` e accumula le misurazioni. 4. Aggiorna il contatore di progresso ad ogni giorno completato. 5. Supporta la cancellazione: controlla un flag `cancelled` ad ogni iterazione; se attivo, interrompe il loop senza scaricare nulla. 6. Al termine, se ci sono misurazioni e il download non è stato annullato, genera e scarica il CSV. ### `DownloadProgress.svelte` Mostra il progresso durante il download: giorno corrente, totale giorni e percentuale. Ha un pulsante "Annulla" che emette un evento verso `StationRow`. --- ## Edge case e comportamenti attesi | Situazione | Comportamento | |-----------------------------------------------|-----------------------------------------------------------------------| | Giorno senza misurazioni (`misure: []`) | Ignorare, continuare con il giorno successivo | | Errore HTTP su un singolo giorno | `fetchDay` restituisce `[]`, ignorare e continuare | | Download annullato dall'utente | Il loop si interrompe al controllo del flag, nessun file scaricato | | Nessuna misurazione dopo tutti i giorni | Non generare il CSV, 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 | `App.svelte` mostra un messaggio di errore all'utente | --- ## Design Il portale riguarda dati ambientali e scientifici raccolti sul campo da stazioni fisiche. L'estetica deve riflettere questo contesto: strumentale, precisa, leggibile. Scegliere una direzione stilistica netta — non generica. Evitare palette pastello, font di sistema e layout cookie-cutter. Curare tipografia, spaziatura e la gerarchia visiva della tabella.