- brut: _merged/index.json has 586 activities — the count when merge_all last ran. The SSE rebuild bug (already fixed) meant it never re-ran after the full Strava sync

added 3256 more.
  - danilo: _merged/ is 8 KB — basically empty. merge_all likely ran concurrently (multiple file uploads trigger multiple rebuilds without a lock in --no-build mode),
  causing a race where shutil.rmtree(merged_acts) from one run wiped what another run was writing.

  Two fixes: serialize --no-build rebuilds with the same lock, and add a "Rebuild" button to the admin page.

 Root causes fixed:
  1. merge_all race condition — --no-build rebuilds now hold _rebuild_lock, same as full builds
  2. The SSE rebuild-trigger bug (already fixed earlier) was brut's original cause
This commit is contained in:
Davide Scaini
2026-04-13 12:35:05 +02:00
parent 7b37f45180
commit 1587d1cdf3
2 changed files with 66 additions and 14 deletions
+39 -4
View File
@@ -115,15 +115,50 @@ import Base from '../../layouts/Base.astro';
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">${u.merged_mb > 0 ? fmt(u.merged_mb) : '—'}</td>
<td class="px-4 py-3 text-right text-zinc-400 tabular-nums">${u.images_mb > 0 ? fmt(u.images_mb) : '—'}</td>
<td class="px-4 py-3 text-right">
<button
class="delete-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-red-900 hover:text-red-300 text-zinc-400 transition-colors"
data-handle="${u.handle}"
>Delete activities</button>
<div class="flex gap-2 justify-end">
<button
class="rebuild-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 transition-colors"
data-handle="${u.handle}"
title="Re-run merge_all and trigger a site rebuild"
>Rebuild</button>
<button
class="delete-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-red-900 hover:text-red-300 text-zinc-400 transition-colors"
data-handle="${u.handle}"
>Delete activities</button>
</div>
</td>
</tr>
`;
}).join('');
tbodyEl.querySelectorAll<HTMLButtonElement>('.rebuild-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const h = btn.dataset.handle!;
btn.disabled = true;
btn.textContent = 'Queued…';
try {
const r = await fetch(`/api/admin/users/${h}/rebuild`, {
method: 'POST',
credentials: 'include',
});
if (r.ok) {
btn.textContent = 'Rebuilding…';
btn.classList.add('text-blue-400');
// Rebuild is async — reload sizes after a delay
setTimeout(() => load(), 8000);
} else {
btn.disabled = false;
btn.textContent = 'Error';
btn.classList.add('text-red-400');
}
} catch {
btn.disabled = false;
btn.textContent = 'Error';
btn.classList.add('text-red-400');
}
});
});
tbodyEl.querySelectorAll<HTMLButtonElement>('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => {
pendingHandle = btn.dataset.handle!;