- Select mode in ActivityFeed: toggle with Select button (logged-in only),
cards become clickable with checkmark indicator, action bar fixed at bottom
- Bulk delete: calls existing DELETE /api/activity/{id} for each selected,
removes from local feed state immediately
- Bulk merge: POST /api/merge sorts by started_at (earliest = primary),
sums distance/duration/elevation, weighted-averages HR/power, concatenates
geojson and timeseries; backs up originals to _merge_backup/ for recovery
- GET /api/merges returns per-user hidden list; feed filters secondaries
client-side on load so static shards don't need a rebuild to hide them
- POST /api/unmerge/{id} restores primary from backup, unhides secondaries
- ActivityDetail: shows "Merged (N)" badge + Unmerge button for owners
- Fix: edit button now works from personal profile feed (handle was missing
from year-shard activities; injected from filterHandle on sessionStorage write)
Adds a List/Map toggle to the feed and @user profile pages. The map view
plots all filtered activities as sport-coloured tracks on a MapLibre map
with no extra requests (uses preview_coords already in memory). Clicking
a track or list row selects it: pans the map to fit, expands the list
item with key stats, and scrolls it into view.
Write the activity summary to sessionStorage on click in ActivityFeed,
then read it synchronously at module init in ActivityDetailLoader so the
page renders immediately without the "Loading activity…" blank screen
or the 2 round-trip index fetch.
Direct URL / bookmark / shared link falls through to the existing slow
path unchanged.
feed.json is now a BAS shard index pointing to feed-YYYY-MM.json files
(~150 activities / ~25 KB gzip each) instead of 400+ sequential feed-N.json
pages. The frontend can now jump directly to a specific month when filtering
by year or date range, without loading all newer data first.
- merge.py: write_combined_feed groups by YYYY-MM and emits a shard index
- dataloader.ts: isYearShardUrl matches feed-YYYY-MM.json; loadCombinedFeed
returns pendingShards; FeedPage interface and loadCombinedFeedPage removed
- ActivityFeed.svelte: _yearFromShard handles both index-YYYY and feed-YYYY-MM;
feedNextPage/feedTotalPages/loadingAllFeedPages removed; infinite-loop bug
fixed (toLoad.length guard before setting loadingAllShards); onMount uses
pendingShards from loadCombinedFeed
- Stop fetching combined-feed pages once the oldest activity in a batch predates
the from-date (feed is newest-first, so everything needed is already loaded)
- Show "Loading…" instead of "No activities found" while eager-load is in progress
- Constrain From max to customTo (or today) and To min to customFrom so the
range can't be inverted via the date pickers
Previous attempt used dateFrom (a derived $: variable) as the trigger which
Svelte 5 doesn't reliably track as a dependency of a side-effect $: block.
Replace with the primary let-variables (customFrom, customTo, datePre) that
Svelte does track statically.
Also extend eager-loading to cover the global combined feed (feedNextPage)
so date/search filtering works on multi-user instances too, not just per-user
profile pages (pendingShards).
The initial page load only fetches the most recent year shard. Selecting a
date range or year preset that spans an older shard returned no results because
those shards were never loaded. Extend the existing search eager-load trigger
to also fire on any non-empty dateFrom, covering both custom date inputs and
year preset buttons.
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.
The counter now shows "50 of 16398 activities" using the total from
feed.json, matching the previous behaviour where all activities were
loaded upfront.
Instead of the browser resolving 20+ user shards recursively (~27 MB),
generate a pre-sorted feed.json at merge time with 50 activities per
page. The global feed loads one ~30 KB file on first paint; "Load more"
fetches subsequent pages (feed-2.json, feed-3.json, etc.).
Per-user profile pages still use year-sharded loadIndexPaged as before.
merge_all/_merged/index.json is now a shard manifest; activities are
split into index-{year}.json files. The feed loads only the most-recent
year on first paint (~200 activities instead of all of them). Older
years are fetched lazily when the user clicks "Load older activities".
Also strips best_efforts / best_climb_m / source from shard files —
these fields are aggregation inputs only, never read by the feed UI.
- "unlisted" = not shown in the public feed, but GPS track, timeseries
and detail JSON are all accessible by direct URL (security by obscurity)
- "private" accepted as legacy alias everywhere (backward compat with
existing data on disk)
- New writes from Strava sync / ZIP upload / sidecar use "unlisted"
- Only "no_gps" now suppresses the GPS track
- isUnlisted() helper in format.ts used by all Svelte/Astro components
- SCHEMA.md and CLAUDE.md document the privacy model and the distinction
between "unlisted" and "no_gps"
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
that covers the whole card making it clickable, and @handle is a proper <a> with z-index: 10 sitting above the stretched link. Clicking the handle navigates to /u/{handle}/; clicking
anywhere else navigates to the activity.
ActivityDetail — @handle link added in the date/time row of the header, linking to /u/{handle}/. Only shown when activity.handle is set (i.e. multi-user mode).
Auth wall (Base.astro): set data-auth-pending on <body> at SSG time and hide
it with inline CSS before any JS runs; remove the attribute after /api/me
resolves. Eliminates the flash of protected content on private instances.
Multi-user write API (serve/server.py): the previous _apply_sidecar_edit and
strava_sync imports from bincio.edit.server were broken (those names don't
exist as module-level exports) and the Strava sync mutated a global data_dir,
making concurrent requests from different users racy. Fix: extract both
operations into bincio/edit/ops.py as pure functions that take data_dir
explicitly. Both edit/server.py and serve/server.py now import from there.
Security: add rate limiting to POST /api/register (5 attempts / 15 min / IP,
separate bucket from login). Add _check_id() activity ID validation to both
GET and POST /api/activity/{id} in serve/server.py.
Single-user mode: _write_root_manifest now forces instance.private=false when
no instance.db exists, even if a previous run wrote true. Prevents the auth
wall from firing and redirecting to /login/ when bincio serve isn't running.
ActivityFeed: skip filterHandle when profileIndexUrl is set (per-user profile
pages load the right shard directly; activities have no handle tag at that
point, so the filter was producing an empty feed). Fix handle links to point
to /u/{handle}/ instead of /{handle}/. Fix <a>-inside-<a> Svelte warning by
converting the inner handle link to a <button>.
- Replace rdp dependency with inline pure-Python RDP implementation
so the bincio wheel runs in Pyodide (no pure-Python wheel existed for rdp)
- Fix convert page script: remove define:vars so Vite bundles it and
TypeScript imports (localstore, format) work correctly
- Rename wheel to proper PEP 427 filename (bincio-0.1.0-py3-none-any.whl)
- Use en-GB date format on convert result, consistent with the feed
- Add /activity/local/ page + LocalActivityDetail for IDB-only activities;
feed links local activities there instead of the SSG route
- Fix getStaticPaths: try public/data symlink as fallback, never crash on
missing index.json
- Fix ActivityDetail.onMount: load detail even when detail_url is absent
so locally converted activities show map and charts
- Derive track_url and detail_url from id in toSummary() since they are
not present in the detail JSON
- Reload on bfcache restore (pageshow) so client:only components re-mount
after back navigation