explore: shard tracks into per-year files for progressive loading

bake_tracks now writes tracks_YYYY.json shards + tracks_index.json manifest
instead of a single monolithic tracks.json. API /api/me/tracks returns the
manifest; /api/me/tracks/{year} serves individual shards. Explore.svelte
fetches the two most recent years eagerly then streams the rest in the
background so the map renders immediately with recent data.
This commit is contained in:
Davide Scaini
2026-05-14 18:34:53 +02:00
parent 8af6b7b04e
commit 5167f2a988
3 changed files with 119 additions and 14 deletions
+51 -3
View File
@@ -15,6 +15,9 @@
let tracks: Track[] = [];
let loading = true;
let loadingYears = false; // background years still fetching
let loadedCount = 0; // years fetched so far
let totalYears = 0; // total years in manifest
let error = '';
// Filters
@@ -69,8 +72,8 @@
$: allTypes = [...new Set(tracks.map(t => t.type))].sort();
$: availableYears = [...new Set(tracks.map(t => t.date.slice(0,4)).filter(Boolean))].sort().reverse();
let typesInitialized = false;
$: if (allTypes.length > 0 && !typesInitialized) { selectedTypes = new Set(allTypes); typesInitialized = true; }
// Auto-select any type that hasn't been seen before (handles progressive loading)
$: { const s = new Set(selectedTypes); let changed = false; for (const t of allTypes) { if (!s.has(t)) { s.add(t); changed = true; } } if (changed) selectedTypes = s; }
$: filteredTracks = tracks.filter(t => {
if (!selectedTypes.has(t.type)) return false;
@@ -199,15 +202,59 @@
map.fitBounds([[w,s],[e,n]], { padding: 40, maxZoom: 14 });
}
async function _loadYears(years: string[]) {
for (const year of years) {
try {
const r = await fetch(`/api/me/tracks/${year}`, { credentials: 'include' });
if (!r.ok) continue;
const d = await r.json();
const newTracks: Track[] = d.tracks ?? [];
if (newTracks.length > 0) {
tracks = [...tracks, ...newTracks];
// reactive $: statement picks up the tracks change and updates the map
}
} catch { /* non-fatal — skip year */ }
loadedCount += 1;
}
loadingYears = false;
}
onMount(async () => {
// Fetch manifest to discover available years
let years: string[] = [];
try {
const r = await fetch('/api/me/tracks', { credentials: 'include' });
if (!r.ok) { error = r.status === 404 ? 'No tracks baked yet — upload activities first.' : `Error ${r.status}`; loading = false; return; }
const d = await r.json();
tracks = d.tracks ?? [];
years = d.years ?? [];
} catch (e: any) { error = e.message ?? 'Failed to load'; loading = false; return; }
if (years.length === 0) { loading = false; return; }
totalYears = years.length;
// Load the two most recent years before showing the map
const eager = years.slice(0, 2);
const lazy = years.slice(2);
for (const year of eager) {
try {
const r = await fetch(`/api/me/tracks/${year}`, { credentials: 'include' });
if (r.ok) {
const d = await r.json();
tracks = [...tracks, ...(d.tracks ?? [])];
}
} catch { /* skip */ }
loadedCount += 1;
}
loading = false;
// Stream remaining years in the background
if (lazy.length > 0) {
loadingYears = true;
_loadYears(lazy);
}
map = new maplibregl.Map({
container: mapEl,
style: { version: 8,
@@ -364,6 +411,7 @@
</section>
{#if loading}<p class="status">Loading tracks…</p>{/if}
{#if !loading && loadingYears}<p class="status">Loading history… {loadedCount}/{totalYears}</p>{/if}
{#if error}<p class="status error">{error}</p>{/if}
</aside>