Commit Graph

29 Commits

Author SHA1 Message Date
Davide Scaini 4641ca9b72 fix: show Select button only on own profile feed, not community feed 2026-06-03 11:11:47 +02:00
Davide Scaini b781193d44 feat: bulk delete + merge activities in feed
- 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)
2026-06-03 10:32:02 +02:00
Davide Scaini 13859a34d3 feat: show pace (min/km) for running, hiking, walking, other activities
Cycling keeps km/h; pace sports show e.g. "5:30 /km" in the feed card,
activity stat panel (avg/max), and laps table.
2026-06-02 16:23:03 +02:00
Davide Scaini df025873c6 Add map view toggle to activity feed
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.
2026-05-22 11:47:47 +02:00
Davide Scaini 29c6e399c0 perf: skip feed index fetch when navigating from activity feed
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.
2026-05-19 19:44:20 +02:00
Davide Scaini fe437626e6 Global feed: switch from sequential pages to month-based BAS shards
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
2026-05-15 10:25:01 +02:00
Davide Scaini d3bce49445 Feed: eager-load only the year shards needed for the active date filter 2026-05-15 09:32:12 +02:00
Davide Scaini 8a06227243 Feed date filter: early-stop global feed load, fix cross-date validation, show Loading while fetching
- 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
2026-05-15 09:24:02 +02:00
Davide Scaini 1f3f5b3d3b Feed: fix date range eager-load — use primary let-vars, cover feed pages
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).
2026-05-15 09:14:56 +02:00
Davide Scaini 9cc70269f5 Feed: eager-load all year shards when a date filter is active
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.
2026-05-15 08:59:06 +02:00
Davide Scaini afbcaa5011 Feed: cap date range inputs at today to prevent selecting future dates 2026-05-15 08:38:58 +02:00
Davide Scaini c905449114 Feed: add custom date range (From/To) inputs alongside search bar 2026-05-15 08:32:31 +02:00
Davide Scaini fb033e3da2 Search: auto-load all year shards when a query is entered so full history is searched 2026-05-13 21:17:55 +02:00
Davide Scaini cb3c9b6e41 Move search bar above sport/date filters, below page title 2026-05-13 11:54:37 +02:00
Davide Scaini 861748a059 ActivityFeed: add title search bar with URL sync 2026-05-13 11:51:14 +02:00
Davide Scaini f6e9fe8198 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.
2026-04-30 21:23:29 +02:00
Davide Scaini 6491e4fd8c fix: show total activity count in global feed counter
The counter now shows "50 of 16398 activities" using the total from
feed.json, matching the previous behaviour where all activities were
loaded upfront.
2026-04-20 17:12:50 +02:00
Davide Scaini db7047f210 perf: combined feed index for multi-user global feed
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.
2026-04-20 15:31:35 +02:00
Davide Scaini cada2bcb03 perf: year-shard index.json to cut initial load from MBs to ~1 year
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.
2026-04-19 22:21:10 +02:00
Davide Scaini 5ad3aee8f6 rename privacy "private" → "unlisted"; enable GPS for unlisted
- "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"
2026-04-13 18:49:20 +02:00
Davide Scaini cbd5a98cd3 - merge.py: keep private activities in _merged/index.json instead of
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
2026-04-10 23:16:38 +02:00
Davide Scaini 50cdeb3b6e ActivityFeed — replaced the <button> inside <a> (invalid HTML, unreliable) with the stretched-link pattern: the card is now a <div>, the title <a> carries a ::before pseudo-element
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).
2026-04-09 13:02:07 +02:00
Davide Scaini cf7c71b8a3 (opus assessment) Fix auth wall flash, broken multi-user write API, and single-user redirect loop
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>.
2026-04-09 09:19:48 +02:00
Davide Scaini f76cc0ce7e towards multi-user 2026-04-08 19:37:10 +02:00
Davide Scaini 083c67d018 local activity storage and convert page fixes
- 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
2026-04-08 14:14:42 +02:00
Davide Scaini 5bf0f3636c local conversion 2026-04-06 22:25:57 +02:00
Davide Scaini cb345c02a1 preserving navigation 2026-03-30 20:27:34 +02:00
Davide Scaini e71e8783ab added skiing 2026-03-29 10:51:26 +02:00
Davide Scaini 3441079913 map now working 2026-03-28 19:34:22 +01:00