fix: delete activity removes it from index.json; detail page uses lazy load

delete_activity now updates data_dir/index.json so merge_all no longer
re-adds the summary for a deleted activity, preventing the broken
"Activity not found" state after deletion.

ActivityDetailLoader switches from loadIndex (all year shards) to
loadIndexPaged (first year shard only) + direct file fallback, so
opening an activity detail page no longer downloads the entire history.
This commit is contained in:
Davide Scaini
2026-04-19 22:31:20 +02:00
parent cada2bcb03
commit 8575a7015b
2 changed files with 71 additions and 35 deletions
+60 -35
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { loadIndex } from '../lib/dataloader';
import { loadIndexPaged } from '../lib/dataloader';
import ActivityDetail from './ActivityDetail.svelte';
import { isUnlisted } from '../lib/format';
import type { ActivitySummary, BASIndex } from '../lib/types';
@@ -12,11 +12,54 @@
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.
* Build an ActivitySummary stub from a detail JSON object.
* Used when we fetch the detail file directly without going through the index.
*/
function summaryFromDetail(d: any, detailUrl: string, handle?: string): ActivitySummary {
return {
id: d.id,
title: d.title ?? d.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: detailUrl,
track_url: d.bbox && d.privacy !== 'no_gps'
? detailUrl.replace(/\.json$/, '.geojson')
: null,
preview_coords: null,
...(handle ? { handle } : {}),
};
}
/**
* Fallback: fetch the activity detail file directly without loading the index.
* Tries single-user path first, then each multi-user handle shard.
*/
async function fetchActivityDirect(id: string): Promise<ActivitySummary | null> {
// Single-user: public/data → _merged/, so activities/ resolves directly
try {
const url = `${base}data/activities/${id}.json`;
const r = await fetch(url);
if (r.ok) {
const d = await r.json();
if (d.id === id) return summaryFromDetail(d, `activities/${id}.json`);
}
} catch { /* fall through */ }
// Multi-user: try each handle shard
try {
const r = await fetch(`${base}data/index.json`);
if (!r.ok) return null;
@@ -28,34 +71,14 @@
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 !== 'no_gps'
? `${shard.handle}/_merged/activities/${id}.geojson`
: null,
preview_coords: null,
handle: shard.handle,
};
} catch { /* try next shard */ }
if (d.id === id) {
return summaryFromDetail(
d,
`${shard.handle}/_merged/activities/${id}.json`,
shard.handle,
);
}
} catch { /* try next */ }
}
} catch { /* ignore */ }
return null;
@@ -68,12 +91,14 @@
if (!id) { notFound = true; loading = false; return; }
try {
const index = await loadIndex(base);
// Load only the most-recent year shard — avoids downloading all years just
// to look up one activity. Falls back to a direct file fetch if not found.
const { index } = await loadIndexPaged(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.
// Not in first year shard (old activity) or shard fetch failed —
// fetch the detail file directly to avoid loading all remaining shards.
activity = await fetchActivityDirect(id);
}