diff --git a/site/src/components/ActivityDetailLoader.svelte b/site/src/components/ActivityDetailLoader.svelte index a0482c7..600113b 100644 --- a/site/src/components/ActivityDetailLoader.svelte +++ b/site/src/components/ActivityDetailLoader.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { loadIndex } from '../lib/dataloader'; import ActivityDetail from './ActivityDetail.svelte'; - import type { ActivitySummary } from '../lib/types'; + import type { ActivitySummary, BASIndex } from '../lib/types'; export let base: string = '/'; @@ -10,6 +10,56 @@ let notFound = false; let loading = true; + /** + * Fallback: if the activity isn't found in the loaded index (e.g. the user's + * shard fetch failed silently), try to fetch the detail file directly from + * each known user shard's _merged/activities/ directory. + */ + async function fetchActivityDirect(id: string): Promise { + try { + const r = await fetch(`${base}data/index.json`); + if (!r.ok) return null; + const root: BASIndex = await r.json(); + for (const shard of (root.shards ?? [])) { + if (!shard.handle) continue; + const url = `${base}data/${shard.handle}/_merged/activities/${id}.json`; + try { + const dr = await fetch(url); + if (!dr.ok) continue; + const d = await dr.json(); + if (d.id !== id) continue; + return { + id: d.id, + title: d.title ?? id, + sport: d.sport ?? 'other', + sub_sport: d.sub_sport ?? null, + started_at: d.started_at ?? '', + distance_m: d.distance_m ?? null, + duration_s: d.duration_s ?? null, + moving_time_s: d.moving_time_s ?? null, + elevation_gain_m: d.elevation_gain_m ?? null, + avg_speed_kmh: d.avg_speed_kmh ?? null, + max_speed_kmh: d.max_speed_kmh ?? null, + avg_hr_bpm: d.avg_hr_bpm ?? null, + max_hr_bpm: d.max_hr_bpm ?? null, + avg_cadence_rpm: d.avg_cadence_rpm ?? null, + avg_power_w: d.avg_power_w ?? null, + mmp: d.mmp ?? null, + source: d.source ?? null, + privacy: d.privacy ?? 'public', + detail_url: `${shard.handle}/_merged/activities/${id}.json`, + track_url: d.bbox && d.privacy !== 'private' && d.privacy !== 'no_gps' + ? `${shard.handle}/_merged/activities/${id}.geojson` + : null, + preview_coords: null, + handle: shard.handle, + }; + } catch { /* try next shard */ } + } + } catch { /* ignore */ } + return null; + } + onMount(async () => { // Extract activity ID from the URL path: /activity/{id}/ const match = window.location.pathname.match(/\/activity\/([^/]+)/); @@ -19,6 +69,13 @@ try { const index = await loadIndex(base); activity = index.activities.find(a => a.id === id) ?? null; + + if (!activity) { + // Shard lookup failed — try fetching the detail file directly. + // This handles transient shard fetch errors, stale index state, etc. + activity = await fetchActivityDirect(id); + } + if (!activity) notFound = true; } catch { notFound = true; diff --git a/site/src/lib/dataloader.ts b/site/src/lib/dataloader.ts index 46b9f40..f427981 100644 --- a/site/src/lib/dataloader.ts +++ b/site/src/lib/dataloader.ts @@ -99,6 +99,13 @@ async function resolveShards( }), ); + // Log shard fetch failures to help diagnose missing-activity issues + shardResults.forEach((r, i) => { + if (r.status === 'rejected') { + console.error('[bincio] shard fetch failed:', index.shards[i]?.url, r.reason); + } + }); + const own = index.activities ?? []; const fromShards = shardResults.flatMap(r => r.status === 'fulfilled' ? r.value : []); return [...own, ...fromShards];