Added SSR data loading, server API proxy with sanitization and HTTP caching

This commit is contained in:
2026-05-08 13:15:06 +02:00
parent 11bddcac70
commit 2e47984bef
7 changed files with 60 additions and 37 deletions
+9 -6
View File
@@ -1,5 +1,5 @@
export async function fetchStations() { export async function fetchStations(fetch = globalThis.fetch) {
const res = await fetch('/api/campagne_map_data'); const res = await fetch('http://37100lab.it:8101/api/campagne_map_data');
if (!res.ok) throw new Error(`Errore HTTP ${res.status}`); if (!res.ok) throw new Error(`Errore HTTP ${res.status}`);
const geojson = await res.json(); const geojson = await res.json();
return geojson.features return geojson.features
@@ -7,14 +7,17 @@ export async function fetchStations() {
.sort((a, b) => b.pk - a.pk); .sort((a, b) => b.pk - a.pk);
} }
export async function fetchDay(id, date, signal) { export async function fetchDay(id, day, signal) {
// Converte YYYY-M-D (da formatDayParam) in YYYY-MM-DD per la chiamata API
const [y, m, d] = day.split('-');
const date = `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
// L'API risponde con 5xx transienti: tre tentativi limitano i buchi nel dataset scaricato. // L'API risponde con 5xx transienti: tre tentativi limitano i buchi nel dataset scaricato.
for (let attempt = 0; attempt < 3; attempt++) { for (let attempt = 0; attempt < 3; attempt++) {
try { try {
const res = await fetch(`/api/get_measurements/${id}?day=${date}`, { signal }); const res = await fetch(`/api/get_station_data?id=${id}&date=${date}`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); return await res.json();
return data.misure ?? [];
} catch (e) { } catch (e) {
// La cancellazione non è un errore: uscire subito senza consumare altri retry. // La cancellazione non è un errore: uscire subito senza consumare altri retry.
if (e.name === 'AbortError') return []; if (e.name === 'AbortError') return [];
+1 -1
View File
@@ -3,7 +3,6 @@
import { getDayRange, formatDayParam } from '$lib/timezone.js'; import { getDayRange, formatDayParam } from '$lib/timezone.js';
import { fetchDay } from '$lib/api.js'; import { fetchDay } from '$lib/api.js';
import { downloadCsv, buildFilename } from '$lib/csv.js'; import { downloadCsv, buildFilename } from '$lib/csv.js';
import { buildCsvAsync } from '$lib/csv-pool.js';
import { enqueue } from '$lib/download-pool.js'; import { enqueue } from '$lib/download-pool.js';
export let station; export let station;
@@ -31,6 +30,7 @@
$: pct = totalDays > 0 ? (currentDay / totalDays) * 100 : 0; $: pct = totalDays > 0 ? (currentDay / totalDays) * 100 : 0;
async function startDownload() { async function startDownload() {
const { buildCsvAsync } = await import('$lib/csv-pool.js');
// Un controller per sessione: abort() cancella tutte le fetch di questa centralina // Un controller per sessione: abort() cancella tutte le fetch di questa centralina
// senza interferire con i download paralleli di altre stazioni. // senza interferire con i download paralleli di altre stazioni.
controller = new AbortController(); controller = new AbortController();
-1
View File
@@ -1 +0,0 @@
export const ssr = false;
+9
View File
@@ -0,0 +1,9 @@
import { fetchStations } from '$lib/api.js';
export async function load({ fetch }) {
try {
return { stations: await fetchStations(fetch) };
} catch {
return { stations: [], loadError: 'Impossibile caricare le centraline. Controlla la connessione e riprova.' };
}
}
+4 -20
View File
@@ -1,21 +1,7 @@
<script> <script>
import { onMount } from 'svelte';
import { fetchStations } from '$lib/api.js';
import StationList from '$lib/components/StationList.svelte'; import StationList from '$lib/components/StationList.svelte';
let stations = []; export let data;
let loading = true;
let error = null;
onMount(async () => {
try {
stations = await fetchStations();
} catch (e) {
error = 'Impossibile caricare le centraline. Controlla la connessione e riprova.';
} finally {
loading = false;
}
});
</script> </script>
<div class="page"> <div class="page">
@@ -25,12 +11,10 @@
</header> </header>
<main> <main>
{#if loading} {#if data.loadError}
<p class="status">Caricamento…</p> <p class="status error">{data.loadError}</p>
{:else if error}
<p class="status error">{error}</p>
{:else} {:else}
<StationList {stations} /> <StationList stations={data.stations} />
{/if} {/if}
</main> </main>
-9
View File
@@ -1,9 +0,0 @@
export async function GET({ params, url }) {
const res = await fetch(`http://37100lab.it:8101/api/${params.path}${url.search}`);
return new Response(res.body, {
status: res.status,
headers: {
'content-type': res.headers.get('content-type') ?? 'application/json',
},
});
}
@@ -0,0 +1,37 @@
import { dev } from '$app/environment';
function cacheControl(date) {
if (dev) return 'no-store';
const toIso = d => 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 async function GET({ url }) {
const id = url.searchParams.get('id');
const date = url.searchParams.get('date'); // YYYY-MM-DD
// 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');
const misure = (JSON.parse(sanitized).misure ?? [])
.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),
},
});
}