Added SSR data loading, server API proxy with sanitization and HTTP caching
This commit is contained in:
+9
-6
@@ -1,5 +1,5 @@
|
||||
export async function fetchStations() {
|
||||
const res = await fetch('/api/campagne_map_data');
|
||||
export async function fetchStations(fetch = globalThis.fetch) {
|
||||
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
|
||||
@@ -7,14 +7,17 @@ export async function fetchStations() {
|
||||
.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.
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
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}`);
|
||||
const data = await res.json();
|
||||
return data.misure ?? [];
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
// La cancellazione non è un errore: uscire subito senza consumare altri retry.
|
||||
if (e.name === 'AbortError') return [];
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { getDayRange, formatDayParam } from '$lib/timezone.js';
|
||||
import { fetchDay } from '$lib/api.js';
|
||||
import { downloadCsv, buildFilename } from '$lib/csv.js';
|
||||
import { buildCsvAsync } from '$lib/csv-pool.js';
|
||||
import { enqueue } from '$lib/download-pool.js';
|
||||
|
||||
export let station;
|
||||
@@ -31,6 +30,7 @@
|
||||
$: pct = totalDays > 0 ? (currentDay / totalDays) * 100 : 0;
|
||||
|
||||
async function startDownload() {
|
||||
const { buildCsvAsync } = await import('$lib/csv-pool.js');
|
||||
// Un controller per sessione: abort() cancella tutte le fetch di questa centralina
|
||||
// senza interferire con i download paralleli di altre stazioni.
|
||||
controller = new AbortController();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const ssr = false;
|
||||
@@ -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
@@ -1,21 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { fetchStations } from '$lib/api.js';
|
||||
import StationList from '$lib/components/StationList.svelte';
|
||||
|
||||
let stations = [];
|
||||
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;
|
||||
}
|
||||
});
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
@@ -25,12 +11,10 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{#if loading}
|
||||
<p class="status">Caricamento…</p>
|
||||
{:else if error}
|
||||
<p class="status error">{error}</p>
|
||||
{#if data.loadError}
|
||||
<p class="status error">{data.loadError}</p>
|
||||
{:else}
|
||||
<StationList {stations} />
|
||||
<StationList stations={data.stations} />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user