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() {
|
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 [];
|
||||||
|
|||||||
@@ -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 +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>
|
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -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