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:
@@ -1288,6 +1288,17 @@ async def delete_activity(
|
|||||||
if images_dir.exists():
|
if images_dir.exists():
|
||||||
shutil.rmtree(images_dir)
|
shutil.rmtree(images_dir)
|
||||||
|
|
||||||
|
# Remove from the extract-level flat index so merge_all doesn't re-add
|
||||||
|
# the summary even though the detail file is gone.
|
||||||
|
index_path = dd / "index.json"
|
||||||
|
if index_path.exists():
|
||||||
|
try:
|
||||||
|
idx = json.loads(index_path.read_text(encoding="utf-8"))
|
||||||
|
idx["activities"] = [a for a in idx.get("activities", []) if a.get("id") != activity_id]
|
||||||
|
index_path.write_text(json.dumps(idx, indent=2, ensure_ascii=False))
|
||||||
|
except Exception:
|
||||||
|
pass # corrupt index — merge_all will clean up on next run
|
||||||
|
|
||||||
# Remove from dedup cache so the file can be re-uploaded if needed
|
# Remove from dedup cache so the file can be re-uploaded if needed
|
||||||
cache_path = dd / ".bincio_cache.json"
|
cache_path = dd / ".bincio_cache.json"
|
||||||
if cache_path.exists():
|
if cache_path.exists():
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { loadIndex } from '../lib/dataloader';
|
import { loadIndexPaged } from '../lib/dataloader';
|
||||||
import ActivityDetail from './ActivityDetail.svelte';
|
import ActivityDetail from './ActivityDetail.svelte';
|
||||||
import { isUnlisted } from '../lib/format';
|
import { isUnlisted } from '../lib/format';
|
||||||
import type { ActivitySummary, BASIndex } from '../lib/types';
|
import type { ActivitySummary, BASIndex } from '../lib/types';
|
||||||
@@ -12,26 +12,13 @@
|
|||||||
let loading = true;
|
let loading = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback: if the activity isn't found in the loaded index (e.g. the user's
|
* Build an ActivitySummary stub from a detail JSON object.
|
||||||
* shard fetch failed silently), try to fetch the detail file directly from
|
* Used when we fetch the detail file directly without going through the index.
|
||||||
* each known user shard's _merged/activities/ directory.
|
|
||||||
*/
|
*/
|
||||||
async function fetchActivityDirect(id: string): Promise<ActivitySummary | null> {
|
function summaryFromDetail(d: any, detailUrl: string, handle?: string): ActivitySummary {
|
||||||
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 {
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
title: d.title ?? id,
|
title: d.title ?? d.id,
|
||||||
sport: d.sport ?? 'other',
|
sport: d.sport ?? 'other',
|
||||||
sub_sport: d.sub_sport ?? null,
|
sub_sport: d.sub_sport ?? null,
|
||||||
started_at: d.started_at ?? '',
|
started_at: d.started_at ?? '',
|
||||||
@@ -48,14 +35,50 @@
|
|||||||
mmp: d.mmp ?? null,
|
mmp: d.mmp ?? null,
|
||||||
source: d.source ?? null,
|
source: d.source ?? null,
|
||||||
privacy: d.privacy ?? 'public',
|
privacy: d.privacy ?? 'public',
|
||||||
detail_url: `${shard.handle}/_merged/activities/${id}.json`,
|
detail_url: detailUrl,
|
||||||
track_url: d.bbox && d.privacy !== 'no_gps'
|
track_url: d.bbox && d.privacy !== 'no_gps'
|
||||||
? `${shard.handle}/_merged/activities/${id}.geojson`
|
? detailUrl.replace(/\.json$/, '.geojson')
|
||||||
: null,
|
: null,
|
||||||
preview_coords: null,
|
preview_coords: null,
|
||||||
handle: shard.handle,
|
...(handle ? { handle } : {}),
|
||||||
};
|
};
|
||||||
} catch { /* try next shard */ }
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
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) {
|
||||||
|
return summaryFromDetail(
|
||||||
|
d,
|
||||||
|
`${shard.handle}/_merged/activities/${id}.json`,
|
||||||
|
shard.handle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch { /* try next */ }
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
return null;
|
return null;
|
||||||
@@ -68,12 +91,14 @@
|
|||||||
if (!id) { notFound = true; loading = false; return; }
|
if (!id) { notFound = true; loading = false; return; }
|
||||||
|
|
||||||
try {
|
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;
|
activity = index.activities.find(a => a.id === id) ?? null;
|
||||||
|
|
||||||
if (!activity) {
|
if (!activity) {
|
||||||
// Shard lookup failed — try fetching the detail file directly.
|
// Not in first year shard (old activity) or shard fetch failed —
|
||||||
// This handles transient shard fetch errors, stale index state, etc.
|
// fetch the detail file directly to avoid loading all remaining shards.
|
||||||
activity = await fetchActivityDirect(id);
|
activity = await fetchActivityDirect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user