Compare commits

...

11 Commits

33 changed files with 1253 additions and 215 deletions
+26
View File
@@ -0,0 +1,26 @@
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
container: docker.io/oven/bun:1-alpine
steps:
- name: Installa git
run: apk add --no-cache git
- name: Checkout del codice
uses: actions/checkout@v3
- name: Installa dipendenze
run: bun install --frozen-lockfile
- name: Deploy su Vercel
run: bunx vercel --prod --yes
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+20 -21
View File
@@ -1,24 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+201 -108
View File
@@ -14,48 +14,67 @@ componenti, file, classi CSS, e qualsiasi termine tecnico consolidato (es.
## 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.
Portale web per visualizzare le centraline di monitoraggio ambientale di
37100lab e scaricare i dati storici come archivio ZIP contenente CSV.
**Stack**: Svelte + Vite. Output finale: cartella `dist/` da servire con
qualsiasi web server statico.
**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.
---
## Setup iniziale
Usare il template Svelte ufficiale di Vite. I comandi di sviluppo standard sono
`dev`, `build` e `preview`.
**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
```
portale-centraline/
download-dati-centraline/
├── CLAUDE.md
├── package.json
├── vite.config.js
├── index.html
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
├── flake.nix
├── static/
│ └── favicon.svg
└── src/
├── App.svelte
├── app.css # variabili CSS globali, font, stili base
├── app.html # template HTML (carica Poppins da Google Fonts)
├── app.d.ts
├── 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
│ ├── 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
## 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`.
@@ -69,8 +88,7 @@ Campi rilevanti di ogni `.properties`:
| `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.
Il campo `pk` va convertito a numero intero.
### GET `/api/get_measurements/{id}?day=YYYY-M-D` — misurazioni giornaliere
@@ -79,8 +97,9 @@ decrescente.
-`2026-5-6`
-`2026-05-06`
La risposta contiene una chiave `misure` con un array di oggetti. Ogni oggetto
ha:
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 |
|--------|---------|-----------------------|
@@ -89,135 +108,209 @@ ha:
| `pm10` | number | PM10 (µg/m³) |
| `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
```
"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
```
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.3
```
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 |
|-------------------|--------------------------------------------------------------|
| Encoding | **UTF-8 con BOM** (`\uFEFF`) — necessario per Excel italiano |
| 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 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) |
| 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 `_`.
Libreria di compressione: `jszip` (unica dipendenza runtime).
---
## Moduli
### `lib/api.js`
### `lib/types.ts`
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.
Interfacce TypeScript condivise: `Station` e `Measurement`.
### `lib/timezone.js`
### `lib/api.ts`
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).
- **`fetchStations(fetch?)`**: carica la lista centraline dal backend esterno.
Accetta il `fetch` di SvelteKit per il SSR. Lancia eccezione in caso di
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
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`.
### `lib/timezone.ts`
Gestisce date e timezone senza librerie esterne.
- **`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
`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.
`YYYY-M-D` senza zero-padding, tramite `Intl.DateTimeFormat` con
`formatToParts`.
- **`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.
- **`getDayRange(startIsoUtc, endIsoUtc)`**: restituisce un array di `Date`,
uno per ogni giorno dal giorno di `startIsoUtc` al giorno di `endIsoUtc`
(estremi inclusi) in `Europe/Rome`. Se `endIsoUtc` è `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.js`
### `lib/csv.ts`
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.
- **`buildCsv(measurements, includeDate?)`**: costruisce la stringa CSV con BOM.
Se `includeDate` è `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)`**: 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
### `App.svelte`
### `routes/+page.server.ts`
Componente radice. Al montaggio carica la lista delle centraline e gestisce i
tre stati: caricamento, errore, lista pronta.
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`
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.
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`
Gestisce la riga di una singola centralina e contiene tutta la logica di
download:
Card di una singola centralina. 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.
1. Calcola il range di giorni con `getDayRange` (limite superiore: `ended_at` o
oggi per le attive).
2. Scarica tutti i giorni **in parallelo** tramite `enqueue` dal pool
(max 3 richieste simultanee) con `Promise.all`.
3. La cancellazione avviene tramite `AbortController`: `controller.abort()`
interrompe le fetch in corso; `fetchDay` intercetta `AbortError` e ritorna `[]`.
4. Tiene traccia del progresso con i contatori `currentDay` / `totalDays`
(incrementati al completamento di ogni giorno).
5. Al completamento di tutte le fetch (non cancellate), genera lo ZIP con `jszip`
e lo scarica.
6. 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.
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`
Mostra il progresso durante il download: giorno corrente, totale giorni e
percentuale. Ha un pulsante "Annulla" che emette un evento verso `StationRow`.
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, 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 |
| 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
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.
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}`).
+19
View File
@@ -30,3 +30,22 @@ bun run dev
bun run build
bun run preview
```
## Gruppi di centraline
Le centraline vengono raggruppate visivamente tramite il file
`src/lib/groups.json`. La struttura è un array di gruppi:
```json
[
{
"nome": "Mantova",
"ids": [151, 154, 155, 158, 159, 173, 174, 176, 177, 178, 179, 180]
}
]
```
Ogni gruppo ha un nome e una lista di ID numerici delle centraline. Le
centraline non presenti in nessun gruppo vengono mostrate automaticamente nella
sezione "Altre". L'ordine dei gruppi nel file determina l'ordine di
visualizzazione sulla pagina.
+74 -2
View File
@@ -4,10 +4,18 @@
"workspaces": {
"": {
"name": "download-dati-centraline",
"dependencies": {
"jszip": "^3.10.1",
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.4",
"vite": "^8.0.10",
"@types/node": "^25.6.2",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7",
},
},
},
@@ -32,6 +40,8 @@
"@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="],
@@ -64,14 +74,24 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.59.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.1.2", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -80,8 +100,14 @@
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -96,8 +122,20 @@
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
@@ -126,26 +164,60 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"rolldown": ["rolldown@1.0.0-rc.18", "", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="],
"svelte-check": ["svelte-check@4.4.8", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@8.0.11", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Download Dati Centraline</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
-33
View File
@@ -1,33 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"types": ["vite/client"],
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}
+25 -14
View File
@@ -1,16 +1,27 @@
{
"name": "download-dati-centraline",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.4",
"vite": "^8.0.10"
}
"name": "download-dati-centraline",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.6.2",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7"
},
"dependencies": {
"jszip": "^3.10.1"
}
}
-2
View File
@@ -1,2 +0,0 @@
<main>
</main>
+52 -4
View File
@@ -2,9 +2,57 @@
box-sizing: border-box;
}
body {
margin: 0;
font-synthesis: none;
text-rendering: optimizeLegibility;
:root {
--bg: #f0f2f5;
--surface: #ffffff;
--border: #dde1e7;
--text: #374151;
--text-dim:#9ca3af;
--text-hi: #111827;
--accent: #2563eb;
--sans: 'Poppins', system-ui, sans-serif;
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
body {
margin: 0;
min-height: 100svh;
}
.tooltip-floating {
position: fixed;
z-index: 9999;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
font-family: var(--sans);
font-size: 0.85rem;
font-weight: 400;
white-space: nowrap;
padding: 0.45em 0.9em;
border-radius: 8px;
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.15s ease, transform 0.15s ease;
}
.tooltip-floating::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--border);
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Centraline 37100Lab</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
import type { Station, Measurement } from './types.js';
export async function fetchStations(fetch = globalThis.fetch): Promise<Station[]> {
const res = await fetch('http://37100lab.it:8101/api/campagne_map_data');
if (!res.ok) throw new Error(`Errore HTTP ${res.status}`);
const geojson = await res.json();
return (geojson.features as Array<{ properties: Record<string, unknown> }>)
.map(f => ({
pk: parseInt(f.properties.pk as string, 10),
title: f.properties.title as string,
created_at: f.properties.created_at as string,
ended_at: f.properties.ended_at as string | null,
}))
.sort((a, b) => b.pk - a.pk);
}
export async function fetchDay(id: number, day: string, signal: AbortSignal): Promise<Measurement[]> {
const [y, m, d] = day.split('-');
const date = `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
for (let attempt = 0; attempt < 3; attempt++) {
try {
const res = await fetch(`/api/get_station_data?id=${id}&date=${date}`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json() as Measurement[];
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') return [];
if (attempt === 2) return [];
}
}
return [];
}
+68
View File
@@ -0,0 +1,68 @@
<script lang="ts">
import StationRow from './StationRow.svelte';
import groups from '$lib/groups.json';
import type { Station } from '$lib/types.js';
let { stations = [] }: { stations: Station[] } = $props();
// Set per lookup O(1): con molte centraline, includes() su array per ogni riga sarebbe O(n²).
const groupedIds = new Set(groups.flatMap(g => g.ids));
// Le attive appaiono prima delle terminate per dare prominenza alle stazioni in funzione.
function sortStations(items: Station[]): Station[] {
return [...items].sort((a, b) => {
if (a.ended_at === null && b.ended_at !== null) return -1;
if (a.ended_at !== null && b.ended_at === null) return 1;
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
});
}
const sections = $derived([
...groups.map(g => ({
name: g.name,
stations: sortStations(stations.filter(s => g.ids.includes(s.pk))),
})).filter(s => s.stations.length > 0),
{
name: 'Altre',
stations: sortStations(stations.filter(s => !groupedIds.has(s.pk))),
},
].filter(s => s.stations.length > 0));
</script>
<div class="content">
{#each sections as section}
<section>
<h2>{section.name}</h2>
<div class="list">
{#each section.stations as station (station.pk)}
<StationRow {station} />
{/each}
</div>
</section>
{/each}
</div>
<style>
.content {
display: flex;
flex-direction: column;
gap: 4rem;
max-width: 820px;
margin: 0 auto;
}
section h2 {
margin: 0 0 0.75rem;
font-size: 0.95rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-dim);
}
.list {
display: flex;
flex-direction: column;
gap: 0.65rem;
}
</style>
+349
View File
@@ -0,0 +1,349 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { getDayRange, formatDayParam } from '$lib/timezone.js';
import { fetchDay } from '$lib/api.js';
import { buildCsv, downloadBlob, buildDayFilename, buildZipFilename } from '$lib/csv.js';
import { enqueue } from '$lib/download-pool.js';
import type { Station, Measurement } from '$lib/types.js';
function floatingTooltip(node: HTMLElement, text: string) {
let el: HTMLDivElement | null = null;
function show() {
if (!text) return;
const rect = node.getBoundingClientRect();
el = document.createElement('div');
el.className = 'tooltip-floating';
el.textContent = text;
document.body.appendChild(el);
el.style.top = `${rect.top - el.offsetHeight - 10}px`;
el.style.left = `${rect.left + rect.width / 2 - el.offsetWidth / 2}px`;
void el.offsetHeight; // force reflow prima della transizione
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
}
function hide() {
if (!el) return;
const target = el;
el = null;
target.style.opacity = '0';
target.style.transform = 'translateY(4px)';
target.addEventListener('transitionend', () => target.remove(), { once: true });
}
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update(t: string) { text = t; },
destroy() {
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
hide();
},
};
}
let { station }: { station: Station } = $props();
const formatDate = (d: string) => new Intl.DateTimeFormat('it-IT', {
timeZone: 'Europe/Rome',
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(new Date(d));
const isActive = $derived(station.ended_at === null);
const createdDate = $derived(formatDate(station.created_at));
const endDate = $derived(isActive ? null : formatDate(station.ended_at!));
let downloading = $state(false);
let cancelling = $state(false);
let completed = $state(false);
let controller: AbortController | null = null;
let currentDay = $state(0);
let totalDays = $state(0);
let pct = $derived(totalDays > 0 ? (currentDay / totalDays) * 100 : 0);
async function startDownload() {
const JSZip = (await import('jszip')).default;
controller = new AbortController();
const { signal } = controller;
downloading = true;
const days = getDayRange(station.created_at, station.ended_at);
totalDays = days.length;
currentDay = 0;
const resultsByDay: Measurement[][] = new Array(days.length);
await Promise.all(days.map((day, idx) =>
enqueue(async () => {
if (signal.aborted) return;
resultsByDay[idx] = await fetchDay(station.pk, formatDayParam(day), signal);
currentDay++;
})
));
if (!signal.aborted) {
currentDay = totalDays;
await new Promise(r => setTimeout(r, 900));
if (!signal.aborted) {
const zip = new JSZip();
let allMeasurements: Measurement[] = [];
for (let i = 0; i < days.length; i++) {
const measurements = resultsByDay[i];
if (!measurements?.length) continue;
zip.file(buildDayFilename(days[i]), buildCsv(measurements, false));
allMeasurements = allMeasurements.concat(measurements);
}
if (allMeasurements.length > 0) {
zip.file(buildZipFilename(station.title, station.pk).replace('.zip', '.csv'), buildCsv(allMeasurements));
const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } });
downloadBlob(blob, buildZipFilename(station.title, station.pk));
completed = true;
await new Promise(r => setTimeout(r, 2000));
}
}
} else {
await new Promise(r => setTimeout(r, 700));
}
downloading = false;
cancelling = false;
completed = false;
controller = null;
}
function cancel() {
cancelling = true;
controller?.abort();
}
// Pulisce le fetch pendenti se il componente viene smontato durante un download.
onDestroy(() => controller?.abort());
</script>
<div class="card">
<div class="left">
<a class="name" href="http://37100lab.it:8101/campagna/{station.pk}" target="_blank" rel="noreferrer">{station.title}</a>
<span class="id">#{station.pk}</span>
</div>
<div class="right">
<span class="date">Attivata il {createdDate}</span>
<span class="sep">·</span>
<span
class="status"
class:active={isActive}
class:ended={!isActive}
use:floatingTooltip={!isActive ? `Terminata il ${endDate}` : ''}
>{isActive ? 'Attiva' : 'Terminata'}</span>
<button
type="button"
title={downloading ? 'Annulla' : 'Scarica CSV'}
onclick={downloading ? cancel : startDownload}
class:downloading
class:completed
>
{#if completed}
<svg width="18" height="18" viewBox="0 0 16 16" fill="none">
<path d="M2 8l4.5 4.5L14 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{:else if downloading}
<span class="icon-download">
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="40 20"/>
</svg>
</span>
<span class="icon-cancel">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
{:else}
<svg width="20" height="20" viewBox="0 0 16 16" fill="none">
<path d="M8 1v9M4.5 6.5 8 10l3.5-3.5M2 12h12" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{/if}
</button>
</div>
{#if downloading}
<div class="bar-wrap">
<div class="bar" class:cancelled={cancelling} class:completed style="width: {Math.max(pct, 2)}%"></div>
</div>
{/if}
</div>
<style>
.card {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.4rem 1.8rem;
display: flex;
align-items: center;
gap: 1.25rem;
overflow: hidden;
}
button {
flex-shrink: 0;
margin-left: 0.75rem;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 8px;
background: transparent;
color: var(--text-dim);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
button:hover {
background: #eff6ff;
border-color: var(--accent);
color: var(--accent);
}
button.downloading {
color: var(--accent);
border-color: var(--accent);
background: #eff6ff;
}
button.downloading .icon-cancel { display: none; }
button.downloading .icon-download { display: flex; }
button.downloading:hover {
background: #fef2f2;
border-color: #dc2626;
color: #dc2626;
}
button.downloading:hover .icon-cancel { display: flex; }
button.downloading:hover .icon-download { display: none; }
.icon-download, .icon-cancel {
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.left {
display: flex;
align-items: baseline;
gap: 1.25rem;
flex: 1;
min-width: 0;
}
.name {
font-size: 1.05rem;
font-weight: 500;
color: var(--text-hi);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.name:hover {
text-decoration: underline;
}
.id {
font-size: 0.75rem;
color: var(--text-dim);
white-space: nowrap;
flex-shrink: 0;
}
.right {
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.sep {
font-size: 0.78rem;
color: var(--text-dim);
opacity: 0.5;
}
.date {
font-size: 0.78rem;
color: var(--text-dim);
white-space: nowrap;
}
.status {
position: relative;
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
}
.active {
color: var(--accent);
}
.ended {
color: var(--text-dim);
}
.bar-wrap {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--border);
overflow: hidden;
}
.bar {
height: 100%;
background: var(--accent);
transition: width 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94), background 0.2s;
}
.bar.cancelled {
background: #dc2626;
}
.bar.completed {
background: #16a34a;
}
button.completed {
color: #16a34a;
border-color: #16a34a;
background: #f0fdf4;
}
button.completed:hover {
color: #16a34a;
border-color: #16a34a;
background: #f0fdf4;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
import type { Measurement } from './types.js';
import { formatTimestampRome, ROME } from './timezone.js';
export function buildCsv(measurements: Measurement[], includeDate = true): string {
const header = includeDate
? '"Data","Ora","Temperatura","Umidità","PM10"'
: '"Ora","Temperatura","Umidità","PM10"';
const rows = measurements.map(m => {
const [date, time] = formatTimestampRome(m.time).split(' ');
const t = time.split('.')[0];
return includeDate ? `"${date}","${t}",${m.temp},${m.umid},${m.pm10}` : `"${t}",${m.temp},${m.umid},${m.pm10}`;
});
// BOM obbligatorio: Excel su Windows interpreta UTF-8 senza BOM come Windows-1252,
// corrompendo i caratteri accentati nelle intestazioni.
return '' + [header, ...rows].join('\n');
}
export function downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
export function buildDayFilename(date: Date): string {
return new Intl.DateTimeFormat('en-CA', { timeZone: ROME }).format(date) + '.csv';
}
export function buildZipFilename(title: string, id: number): string {
const sanitized = title.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
return `${sanitized}_(${id}).zip`;
}
+24
View File
@@ -0,0 +1,24 @@
export const DOWNLOAD_WORKERS = 3;
type Job = { fn: () => Promise<void>; resolve: () => void };
let active = 0;
const queue: Job[] = [];
function next(): void {
while (active < DOWNLOAD_WORKERS && queue.length) {
active++;
const { fn, resolve } = queue.shift()!;
Promise.resolve(fn()).then(resolve).finally(() => {
active--;
next();
});
}
}
export function enqueue(fn: () => Promise<void>): Promise<void> {
return new Promise(resolve => {
queue.push({ fn, resolve });
next();
});
}
+6
View File
@@ -0,0 +1,6 @@
[
{
"name": "Mantova",
"ids": [151, 154, 155, 158, 159, 173, 174, 176, 177, 178, 179, 180]
}
]
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+68
View File
@@ -0,0 +1,68 @@
export const ROME = 'Europe/Rome';
const PAD2 = Array.from({ length: 100 }, (_, i) => String(i).padStart(2, '0'));
const offsetCache = new Map<string, number>();
function getRomeOffsetMs(date: Date): number {
const key = `${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}-${date.getUTCHours()}`;
if (offsetCache.has(key)) return offsetCache.get(key)!;
const romeHour = parseInt(
new Intl.DateTimeFormat('en', { timeZone: ROME, hour: 'numeric', hour12: false }).format(date),
10
);
let diff = romeHour - date.getUTCHours();
if (diff > 12) diff -= 24;
if (diff < -12) diff += 24;
const offsetMs = diff * 3_600_000;
offsetCache.set(key, offsetMs);
return offsetMs;
}
export function formatTimestampRome(isoString: string): string {
const d = new Date(isoString);
const frac = Math.floor(d.getMilliseconds() / 100);
const r = new Date(d.getTime() + getRomeOffsetMs(d));
const dd = PAD2[r.getUTCDate()];
const mo = PAD2[r.getUTCMonth() + 1];
const yyyy = r.getUTCFullYear();
const hh = PAD2[r.getUTCHours()];
const mm = PAD2[r.getUTCMinutes()];
const ss = PAD2[r.getUTCSeconds()];
return `${dd}/${mo}/${yyyy} ${hh}:${mm}:${ss}.${frac}`;
}
export function formatDayParam(date: Date): string {
const parts = new Intl.DateTimeFormat('it-IT', {
timeZone: ROME,
year: 'numeric',
month: 'numeric',
day: 'numeric',
}).formatToParts(date);
const get = (type: string) => parts.find(p => p.type === type)!.value;
return `${get('year')}-${get('month')}-${get('day')}`;
}
export function getDayRange(startIsoUtc: string, endIsoUtc: string | null): Date[] {
const toRomeDateString = (date: Date) => new Intl.DateTimeFormat('en-CA', {
timeZone: ROME,
}).format(date);
const startStr = toRomeDateString(new Date(startIsoUtc));
const endStr = toRomeDateString(endIsoUtc ? new Date(endIsoUtc) : new Date());
const days: Date[] = [];
let cursor = new Date(`${startStr}T12:00:00Z`);
const end = new Date(`${endStr}T12:00:00Z`);
while (cursor <= end) {
days.push(new Date(cursor));
cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);
}
return days;
}
+13
View File
@@ -0,0 +1,13 @@
export interface Station {
pk: number;
title: string;
created_at: string;
ended_at: string | null;
}
export interface Measurement {
temp: number;
umid: number;
pm10: number;
time: string;
}
-9
View File
@@ -1,9 +0,0 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app'),
})
export default app
+6
View File
@@ -0,0 +1,6 @@
<script lang="ts">
import '../app.css';
let { children } = $props();
</script>
{@render children()}
+10
View File
@@ -0,0 +1,10 @@
import type { PageServerLoad } from './$types.js';
import { fetchStations } from '$lib/api.js';
export const load: PageServerLoad = async ({ fetch }) => {
try {
return { stations: await fetchStations(fetch) };
} catch {
return { stations: [], loadError: 'Impossibile caricare le centraline. Controlla la connessione e riprova.' };
}
};
+110
View File
@@ -0,0 +1,110 @@
<script lang="ts">
import StationList from '$lib/components/StationList.svelte';
let { data } = $props();
</script>
<div class="page">
<header>
<h1>Centraline</h1>
<p>Scarica i dati storici delle stazioni di monitoraggio ambientale di <a href="http://37100lab.it:8101/" target="_blank" rel="noreferrer">37100Lab</a>.</p>
</header>
<main>
{#if data.loadError}
<p class="status error">{data.loadError}</p>
{:else}
<StationList stations={data.stations} />
{/if}
</main>
<footer>
<p>Fatto da <strong><a href="https://nicolabelluti.me" target="_blank" rel="noreferrer">Nicola Belluti</a></strong> (e <a href="https://claude.com/product/claude-code" target="_blank" rel="noreferrer">Claude Code</a>) col ❤️</p>
<p class="links">
<a href="https://git.nicolabelluti.me/nicolabelluti/download-dati-centraline" target="_blank" rel="noreferrer">Codice sorgente</a>
·
<a href="https://choosealicense.com/licenses/agpl-3.0/" target="_blank" rel="noreferrer">Licenza GNU AGPLv3.0</a>
</p>
</footer>
</div>
<style>
.page {
max-width: 1100px;
margin: 0 auto;
padding: 7rem 0.5rem 0;
min-height: 100svh;
display: flex;
flex-direction: column;
}
header {
margin-top: 2rem;
margin-bottom: 7rem;
padding-left: 2rem;
}
main {
flex: 1;
padding: 0 0 5rem;
}
h1 {
margin: 0 0 1rem;
font-size: 3.75rem;
font-weight: 500;
color: var(--text-hi);
letter-spacing: -0.04em;
line-height: 1.1;
}
p {
margin: 0;
font-size: 0.9rem;
color: var(--text-dim);
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.status {
font-size: 0.875rem;
color: var(--text-dim);
}
.error {
color: #dc2626;
}
footer {
margin-top: auto;
padding: 2rem 0 4rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
footer p {
font-size: 0.82rem;
}
footer strong {
font-weight: 600;
}
.links {
display: flex;
gap: 0.6rem;
align-items: center;
color: var(--text-dim);
}
</style>
@@ -0,0 +1,43 @@
import { dev } from '$app/environment';
import type { RequestHandler } from './$types.js';
function cacheControl(date: string): string {
if (dev) return 'no-store';
const toIso = (d: Date) => new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Rome' }).format(d);
const yesterday = toIso(new Date(Date.now() - 86_400_000));
return date < yesterday ? 'public, max-age=604800' : 'public, max-age=60';
}
export const GET: RequestHandler = async ({ url }) => {
const id = url.searchParams.get('id');
const date = url.searchParams.get('date'); // YYYY-MM-DD
if (!id || !date) {
return new Response('[]', { status: 400, headers: { 'content-type': 'application/json' } });
}
// Il backend vuole YYYY-M-D senza zero-padding
const [y, m, d] = date.split('-');
const day = `${parseInt(y)}-${parseInt(m)}-${parseInt(d)}`;
const res = await fetch(`http://37100lab.it:8101/api/get_measurements/${id}?day=${day}`);
if (!res.ok) {
return new Response('[]', { status: res.status, headers: { 'content-type': 'application/json' } });
}
const text = await res.text();
// Il backend Python emette NaN/Infinity per valori mancanti o fuori range,
// che non sono JSON valido e farebbero esplodere JSON.parse.
const sanitized = text.replace(/(?<=[:\s\[,])(?:nan|inf|-inf|infinity|-infinity)(?=[,\]\s}])/gi, 'null');
type RawMeasurement = { temp: number; umid: number; pm10: number; time: string };
const misure = ((JSON.parse(sanitized).misure ?? []) as RawMeasurement[])
.filter(m => m != null && Number.isFinite(new Date(m.time).getTime()));
return new Response(JSON.stringify(misure), {
headers: {
'content-type': 'application/json',
'cache-control': cacheControl(date),
},
});
};

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+13 -2
View File
@@ -1,2 +1,13 @@
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {}
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: {
runes: true,
},
kit: {
adapter: adapter()
}
};
export default config;
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
-7
View File
@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
})
+6
View File
@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
});