If the index-based lookup fails (shard fetch silently failed, stale
index state, etc.), try fetching the activity detail file directly from
each user shard's _merged/activities/ directory. This makes private
activities and newly-synced activities accessible even when the index
resolution fails.
Also add console.error logging when shards fail in resolveShards to
help diagnose root causes.
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
next server restart.
Admin page now shows:
- Overall disk bar (used/free/%)
- Per-user table: total, activities (with file count), originals (with Strava breakdown), merged, images
- A mini bar per user showing relative size
- Red ⚠ warning if orphaned temp ZIPs are still present for a user
- Delete activities button (reloads sizes after)
index.json, then triggers a rebuild. Admin-only.
- /admin/ page — lists all users, each with a "Delete activities" button. Clicking asks for
confirmation in a <dialog> before firing the request. Button shows "Deleted (N)" or an error inline.
- "Admin" nav link — appears in the top-right for admins only, hidden for everyone else.
Key at data_dir.parent/.garmin_key — nginx serves location /data/ { alias /var/bincio/data/; } so
anything inside that dir is reachable. The key lives one level up at /var/bincio/.garmin_key,
outside nginx's reach.
Two-layer storage — garmin_creds.json holds the encrypted email+password (needed for re-login when
tokens expire); garmin_session/ holds the garth OAuth tokens in plain JSON (short-lived, not the
user's actual password).
test_login() — called by the connect endpoint before saving anything, so credentials are only
persisted if they actually work.
get_client() — tries the session first (fast, no network), falls back to full re-login
transparently. The caller never needs to think about whether the session is fresh.
- POST /api/upload now returns text/event-stream instead of JSON
- Per-file progress events stream back as each file is processed: ↓ 3/47 (6%) — morning_ride.fit
- Final done event shows the summary: "12 added, 35 duplicates"
- The Vite proxy is configured to stream this properly (no buffering)
For the admin:
- New GET /api/admin/jobs endpoint (admin-only) returns the list of active upload jobs, each with
user, started_at, total, done, current (filename being processed)
- A pulsing amber badge appears in the nav bar for admins when any user has an active upload running
— it shows e.g. "2 uploads running" with a tooltip listing each user's progress (@alice: 12/50
files)
- Polls every 5 seconds, disappears automatically when all jobs finish
stripping them; privacy filtering is now done client-side
- ActivityFeed: detect logged-in user via bincio:me event; show private
activities only when viewing your own profile; private cards get a lock
badge
container div, causing the layout to collapse to zero height for a moment.
The browser then scrolls to keep the viewport anchored, but since the page
got shorter it jumps to the top. When the new SVG is appended, the page is
taller again but the scroll position was already reset.
Fix: give the chart container a min-height matching the chart height (220px)
so it never collapses.
Logo and action buttons are shrink-0 anchors; nav links occupy the
remaining space with overflow-x:auto and a hidden scrollbar so they
scroll independently. body gets overflow-x:hidden to prevent the
whole page from drifting sideways on narrow screens.
last_sync_at timestamp was never written. After deploying, do a soft reset — it'll set last_sync_at to your most recent activity's timestamp so the next sync only fetches newer ones.
- Reset 404: Added POST /api/strava/reset to serve/server.py. The soft reset now looks in _merged/index.json first (multi-user path), falling back to index.json.
1. The build took minutes → 404 during that window
2. Even after the build, the output lands in site/dist/ — nginx serves from /var/www/bincio/ which is only updated by the rsync in the post-receive hook, not by the server process
Fixes applied:
1. bincio/render/cli.py: Added --no-build flag — merges sidecars and updates manifests but skips astro build. This is fast (~1 second).
2. bincio/serve/server.py _trigger_rebuild: Now passes --no-build. After an upload, _merged/ and root index.json are updated immediately, so the feed reflects the new activity. The static Astro pages are
only rebuilt on git push.
3. site/src/components/ActivityDetailLoader.svelte (new): Svelte component that reads the activity ID from the URL, calls loadIndex to resolve the shard tree, then renders ActivityDetail dynamically — no
pre-built page needed.
4. site/src/pages/activity/index.astro (new): Generic Astro shell page that renders ActivityDetailLoader. Gets compiled to dist/activity/index.html.
5. docs/deployment/vps.md: Added location /activity/ { try_files $uri $uri/ /activity/index.html; } to the nginx config. When a request arrives for /activity/2026-04-06T153345Z/ and no pre-built file
exists, nginx serves the shell, which loads the data dynamically from /data/ (which nginx already serves live from disk).