- 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:
+18
-1
@@ -243,7 +243,10 @@ def _trigger_rebuild(handle: str) -> None:
|
|||||||
def _run() -> None:
|
def _run() -> None:
|
||||||
try:
|
try:
|
||||||
if _webroot is None:
|
if _webroot is None:
|
||||||
# Fast: only update data, skip Astro build
|
# Fast: only update data, skip Astro build.
|
||||||
|
# Serialised with the same lock: merge_all wipes and recreates
|
||||||
|
# _merged/activities/ — concurrent runs would corrupt each other.
|
||||||
|
with _rebuild_lock:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
[uv, "run", "bincio", "render",
|
[uv, "run", "bincio", "render",
|
||||||
"--data-dir", _data_dir,
|
"--data-dir", _data_dir,
|
||||||
@@ -500,6 +503,20 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/admin/users/{handle}/rebuild")
|
||||||
|
async def admin_rebuild(
|
||||||
|
handle: str,
|
||||||
|
bincio_session: Optional[str] = Cookie(default=None),
|
||||||
|
) -> JSONResponse:
|
||||||
|
"""Trigger a merge_all + site rebuild for a user. Admin only."""
|
||||||
|
_require_admin(bincio_session)
|
||||||
|
user_dir = _get_data_dir() / handle
|
||||||
|
if not user_dir.is_dir():
|
||||||
|
raise HTTPException(404, f"No data directory for user '{handle}'")
|
||||||
|
_trigger_rebuild(handle)
|
||||||
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/admin/users/{handle}/activities")
|
@app.delete("/api/admin/users/{handle}/activities")
|
||||||
async def admin_delete_activities(
|
async def admin_delete_activities(
|
||||||
handle: str,
|
handle: str,
|
||||||
|
|||||||
@@ -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.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 text-zinc-400 tabular-nums">${u.images_mb > 0 ? fmt(u.images_mb) : '—'}</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
|
<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
|
<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"
|
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}"
|
data-handle="${u.handle}"
|
||||||
>Delete activities</button>
|
>Delete activities</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).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 => {
|
tbodyEl.querySelectorAll<HTMLButtonElement>('.delete-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
pendingHandle = btn.dataset.handle!;
|
pendingHandle = btn.dataset.handle!;
|
||||||
|
|||||||
Reference in New Issue
Block a user