feat(serve): debounced site rebuild — burst uploads trigger one build, not N

Replace per-upload Astro build threads with a single background worker
(_site_rebuild_worker) that waits on an event, sleeps 60 s to let upload
bursts settle, then runs one full build + rsync. 271 concurrent uploads now
produce one build instead of 271 serialised builds, eliminating the OOM kill.
--webroot is re-enabled; merge-only path still runs immediately per upload.

Also: date filter row added to ActivityFeed.svelte (sport + date presets
with dynamic year pills); deploy/vps gitignored for VPS config backups.
This commit is contained in:
Davide Scaini
2026-04-30 21:23:29 +02:00
parent 5e36806392
commit f6e9fe8198
3 changed files with 128 additions and 73 deletions
+48 -5
View File
@@ -39,6 +39,9 @@
let all: ActivitySummary[] = [];
let sport: Sport | 'all' = 'all';
let datePre = 'all';
let dateFrom = '';
let dateTo = '';
let shown = PAGE_SIZE;
let loading = true;
let loadingMore = false;
@@ -53,6 +56,21 @@
/** Logged-in handle — resolved async via bincio:me event. */
let me: string = '';
function computeDateRange(preset: string): { dateFrom: string; dateTo: string } {
if (preset === 'all') return { dateFrom: '', dateTo: '' };
if (/^\d{4}$/.test(preset)) {
const y = parseInt(preset, 10);
return { dateFrom: `${y}-01-01T`, dateTo: `${y + 1}-01-01T` };
}
const pad = (n: number) => String(n).padStart(2, '0');
const now = new Date();
let d: Date;
if (preset === '7d') d = new Date(now.getTime() - 7 * 86_400_000);
else if (preset === '30d') d = new Date(now.getTime() - 30 * 86_400_000);
else { d = new Date(now); d.setMonth(d.getMonth() - 6); }
return { dateFrom: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T`, dateTo: '' };
}
// Show private activities only to their owner.
// On a profile page (filterHandle set): show unlisted if me === filterHandle.
// On the global feed: show unlisted only for the logged-in user's own activities.
@@ -63,7 +81,12 @@
}
return true;
});
$: filtered = sport === 'all' ? withPrivacy : withPrivacy.filter(a => a.sport === sport);
$: allYears = [...new Set(all.map(a => a.started_at?.slice(0, 4)).filter(Boolean) as string[])].sort().reverse();
$: ({ dateFrom, dateTo } = computeDateRange(datePre));
$: withDate = !dateFrom && !dateTo ? withPrivacy : withPrivacy.filter(a =>
(!dateFrom || a.started_at >= dateFrom) && (!dateTo || a.started_at < dateTo)
);
$: filtered = sport === 'all' ? withDate : withDate.filter(a => a.sport === sport);
$: visible = filtered.slice(0, shown);
$: canShowMore = shown < filtered.length;
$: hasMore = canShowMore || pendingShards.length > 0 || feedNextPage > 0;
@@ -99,17 +122,20 @@
}
}
$: if (sport) shown = PAGE_SIZE; // reset pagination on filter change
$: if (sport || datePre) shown = PAGE_SIZE; // reset pagination on filter change
$: if (mounted) {
const params = new URLSearchParams(window.location.search);
if (sport === 'all') params.delete('sport'); else params.set('sport', sport);
if (datePre === 'all') params.delete('date'); else params.set('date', datePre);
const qs = params.toString();
history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname);
}
onMount(async () => {
sport = (new URLSearchParams(window.location.search).get('sport') as Sport | 'all') ?? 'all';
const params = new URLSearchParams(window.location.search);
sport = (params.get('sport') as Sport | 'all') ?? 'all';
datePre = params.get('date') ?? 'all';
mounted = true;
// Resolve the logged-in handle so we can show the owner their private activities.
@@ -161,8 +187,8 @@
];
</script>
<!-- Filter bar -->
<div class="flex gap-2 mb-6 flex-wrap">
<!-- Sport filter bar -->
<div class="flex gap-2 mb-3 flex-wrap">
{#each sports as s}
<button
class="px-3 py-1 rounded-full text-sm font-medium border transition-colors"
@@ -187,6 +213,23 @@
{/if}
</div>
<!-- Date filter bar -->
<div class="flex gap-2 mb-6 flex-wrap">
{#each [{ value: 'all', label: 'All time' }, { value: '7d', label: '7 days' }, { value: '30d', label: '30 days' }, { value: '6mo', label: '6 months' }, ...allYears.map(y => ({ value: y, label: y }))] as d}
<button
class="px-3 py-1 rounded-full text-sm font-medium border transition-colors"
class:border-zinc-700={datePre !== d.value}
class:text-zinc-400={datePre !== d.value}
class:border-[--accent]={datePre === d.value}
class:text-white={datePre === d.value}
style={datePre === d.value ? 'background:var(--accent-dim)' : ''}
on:click={() => datePre = d.value}
>
{d.label}
</button>
{/each}
</div>
{#if loading}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{#each Array(12) as _}