Commit Graph

47 Commits

Author SHA1 Message Date
Davide Scaini 12693dbd60 feat: scheduled Strava sync + admin suspend/delete account
- Add bincio sync-strava command: headless multi-user Strava sync
  designed for systemd timer. Discovers users via strava_token.json,
  skips users without their own strava_credentials.json, respects
  Strava visibility (only_me → unlisted). Treats 404 stream errors as
  no-GPS activities rather than retrying every run.
- Add deploy/systemd/bincio-sync.{service,timer}: runs every 3 hours,
  Persistent=true to catch up after downtime.
- Add POST /api/internal/rebuild: webhook for sync timer to trigger
  site rebuild, authenticated via X-Sync-Secret header.
- Add suspended column to users table with auto-migration on open_db.
  Suspended users are blocked at login and session lookup (covers both
  activity site and wiki, which share instance.db).
- Add POST /api/admin/users/{handle}/suspend|unsuspend and
  DELETE /api/admin/users/{handle}/account endpoints.
- Admin panel: Suspend/Unsuspend toggle, Del account button, suspended
  badge on user row.
2026-05-08 10:36:21 +02:00
Davide Scaini 9540cdd6cb Replace Astro hub build with standalone hub/index.html
CI / Python tests (push) Waiting to run
CI / Frontend build (push) Waiting to run
2026-05-02 22:49:32 +02:00
Davide Scaini 58def4bf02 weird characters 2026-05-01 23:49:48 +02:00
Davide Scaini 12ef5100ef fix(hub): greeting above tagline, no punctuation 2026-05-01 22:08:27 +02:00
Davide Scaini 0ab62aa961 fix(hub): activity card links to /u/{handle}/ when same origin to avoid hub loop 2026-05-01 22:07:01 +02:00
Davide Scaini 1f11bee730 feat: bincio.org hub page (login / app selector) and grants_activity invite toggle 2026-05-01 22:04:30 +02:00
Davide Scaini 82288a35ea feat(auth): wiki/activity access flags, SESSION_DOMAIN, wiki nav link 2026-05-01 21:56:02 +02:00
Davide Scaini 7142ac8f2e settings: split danger zone into delete originals / delete all activities
Move "Delete original files" out of the Storage card and into the
Danger zone as a first, less-destructive step (simple confirm, no
password needed). "Delete all activity data" and "Delete account"
follow below it, both still password-gated. Descriptions clarify
exactly what each action does and does not remove.
2026-04-15 20:42:31 +02:00
Davide Scaini 87a69bcc8b settings: add nav visibility prefs and per-user Strava credentials
- user_prefs table in db.py with get/set helpers
- GET/PUT /api/me/prefs endpoints for bulk pref management
- GET/PUT/DELETE /api/me/strava-credentials; PUT preserves existing
  secret when client_secret field is left blank
- _strava_creds() helper resolves per-user → instance fallback across
  all five Strava endpoints
- Settings page: Navigation card (hide Feed/Community/About toggles)
  and Strava credentials card
- Base.astro: ids on feed/community/about nav links; applies
  nav_hide_* prefs after login
2026-04-15 20:37:42 +02:00
Davide Scaini 4fd5ba428e settings: add self-service user settings page
API endpoints (all auth-gated to the logged-in user):
- GET  /api/me/storage        — per-category disk breakdown
- DELETE /api/me/originals    — free originals/ dir (post-extraction cleanup)
- DELETE /api/me/activities   — wipe all activity data (password confirm)
- DELETE /api/me              — delete account + all data (password confirm)
- PUT  /api/me/display-name   — update display name
- PUT  /api/me/password       — change password (requires current password)

Page at /settings/:
- Storage card: activities / originals / Strava originals / photos / total
  with one-click 'Delete original files' when originals exist
- Profile card: display name field with inline save
- Password card: change password form
- Danger zone: delete all activities or delete account (both require
  password confirmation in a modal before proceeding)

Nav: 'Settings' link appears in the top bar after login (same as Admin).
2026-04-15 20:24:04 +02:00
Davide Scaini a33fea91cf admin: mark ghost users (no DB account) and add Delete dir button
- /api/admin/disk now includes in_db flag per user (true if account exists in DB)
- Ghost users (directory exists, no DB account) show amber 'ghost' badge and only
  Diag + Delete dir buttons (no Re-extract, Rebuild, Reset pwd, Reset data)
- DELETE /api/admin/users/{handle}/directory wipes the entire directory and updates
  the root manifest; refuses if the account still exists in the DB
- Wires up rmdir-btn with a window.confirm before calling the new endpoint
2026-04-15 14:58:54 +02:00
Davide Scaini 378cba85ad fix re-extract: add heartbeat yield, batch index writes, handle HTTP errors in UI
- Generator now yields a 'status' event immediately so the client can
  distinguish 'working' from 'failed silently before first event'
- Batch mode: call write_activity per file but write index.json and
  athlete.json only once at the end (was O(n²) — 2015 rewrites)
- JS: check r.ok before reading the body stream; show HTTP error detail
  instead of staying stuck at 'Starting…'
- Handle 'status' event type in the progress log
2026-04-15 08:57:21 +02:00
Davide Scaini 89b92397cf add re-extract from Strava originals endpoint and improve diag
- POST /api/admin/users/{handle}/reextract-originals: reads stored
  originals/strava/*.json and re-runs strava_to_parsed + ingest_parsed
  without hitting the Strava API; streams SSE progress; calls merge_all
  and rebuild on completion
- GET /api/admin/users/{handle}/diag: now shows _merged/activities/
  file counts, a sample of filenames in activities/ (with symlink flag),
  and lists pending_files by name
- Admin page: Re-extract button per user with live SSE progress modal
2026-04-15 08:08:57 +02:00
Davide Scaini 1e30f85bdc add structured logging and admin diagnostics to serve
- bincio.serve logger wired into uvicorn output: rebuild steps, upload
  errors, strava-zip progress all now appear in the server log
- _trigger_rebuild: capture stdout/stderr, log errors instead of silently
  discarding; exceptions logged with traceback instead of swallowed
- upload handler: log per-file errors with traceback; include error detail
  in the SSE event sent back to the browser
- strava-zip handler: log imported/error counts on completion
- GET /api/admin/users/{handle}/diag: snapshot of a user's data dir
  (file counts, sizes, index activity counts, pending uploads)
- POST /api/admin/users/{handle}/rebuild-sync: blocking rebuild that
  returns full stdout/stderr — for debugging without SSH log access
- Admin page: Diag button per user opens a modal showing the diag JSON
2026-04-14 22:53:31 +02:00
Davide Scaini 9419bd0c20 document password reset flow in CLAUDE.md and reset-password page 2026-04-14 22:34:15 +02:00
Davide Scaini 8fbd9a95e8 stop pre-building activity pages to fix OOM build failure
getStaticPaths now returns [] — all /activity/{id}/ URLs are served by
the activity/index.html shell via nginx try_files and hydrated by
ActivityDetailLoader. Pre-rendering thousands of pages was exhausting
server RAM and killing the build. The dynamic loader already handles
public, unlisted, and local activities identically.
2026-04-14 22:22:34 +02:00
Davide Scaini 13643479ef add password reset via admin-generated one-time code
db.py: reset_codes table (code, handle, created_by, created_at,
expires_at, used_at); create_reset_code() invalidates any prior unused
code for the same handle; use_reset_code() validates handle match,
expiry (24 h), and single-use; change_password() updates the hash.

server.py: POST /api/admin/users/{handle}/reset-password-code (admin)
returns a code; POST /api/auth/reset-password (public) validates the
code + handle and sets the new password.

Admin page: "Reset pwd" button per user — shows the code inline on
click (monospace, click-to-copy).
/reset-password/ page: handle + code + new password form.
Login page: "Forgot password?" link.
2026-04-14 21:58:50 +02:00
Davide Scaini d2ba96c26a fix admin delete to wipe originals/edits/geojson; rename button to Reset data
The old DELETE /api/admin/users/{handle}/activities only removed *.json
files and _merged/, leaving originals/ (Strava FIT files) and edits/
untouched — causing the 968 MB disk usage after a delete.

_wipe_user_activities() now removes activities/, edits/, originals/,
_merged/, index.json, athlete.json, and .bincio_cache.json. Admin page
button renamed to "Reset data" with updated confirmation text.
2026-04-13 20:10:15 +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 1587d1cdf3 - 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
2026-04-13 12:35:05 +02:00
Davide Scaini 7b37f45180 Bug fixed — temp ZIPs now go to /tmp/ (system temp) and are always deleted in a finally block, so they can't leak. A startup hook also auto-cleans any leftovers on
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)
2026-04-13 12:24:59 +02:00
Davide Scaini d659b90cd9 - DELETE /api/admin/users/{handle}/activities — deletes all activities/*.json, wipes _merged/ and
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.
2026-04-12 17:46:28 +02:00
Davide Scaini 2774f436d8 login payoff 2026-04-12 15:47:27 +02:00
Davide Scaini 705e00f852 adding community tab 2026-04-11 14:39:19 +02:00
Davide Scaini bc30e0a2fc option to keep all activities private from strava zip, fix copy of register link 2026-04-10 22:51:29 +02:00
Davide Scaini e5eadc69f2 fix: remove double px-4 on user profile page headers 2026-04-10 19:20:00 +02:00
Davide Scaini eeed3fe3b2 Root cause of the 404: _trigger_rebuild was firing bincio render (= full astro build), but:
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).
2026-04-10 17:48:23 +02:00
Davide Scaini a20df6bd57 fix: hide community stats/tree on about page for non-logged-in users 2026-04-10 17:40:16 +02:00
Davide Scaini ae883a7dba fix: rebuild athlete.json on every ingest; remove bincio-extract references from UI 2026-04-10 15:47:50 +02:00
Davide Scaini e006175285 fix: don't redirect to /u/{handle}/ on private (multi-user) instances 2026-04-10 15:06:45 +02:00
Davide Scaini 4593478863 feedback page 2026-04-10 14:23:31 +02:00
Davide Scaini 8b7cdd9ed1 explain invitation system 2026-04-10 13:49:15 +02:00
Davide Scaini 053da10ab9 some basic statistics and invite tree, plus watch new data 2026-04-10 13:21:31 +02:00
Davide Scaini f37e898eb5 about page 2026-04-10 13:05:51 +02:00
Davide Scaini cbac82a2ba Fix 1 — new user pages 404 (server.py:228):
After registration creates the user's directories, it now calls _write_root_manifest(dd). This rewrites index.json to include the new handle's shard immediately. Since Astro dev re-evaluates getStaticPaths() on every request (reading that file), /u/pres/, /u/pres/stats/, and /u/pres/athlete/ will resolve correctly as soon as the new user navigates there.

  Fix 2 — invites link (athlete/index.astro:33):
  Added an Invites button (top-right, same style as "Edit profile") that starts hidden. When bincio:me fires and me === handle (you're on your own page), the subnav tabs are removed as before AND the invites button is revealed. Other visitors see neither.
2026-04-09 21:44:38 +02:00
Davide Scaini 084c652fdd fixing stuff after splitting jsons 2026-04-09 15:27:00 +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 509557ed6f fix: the user feed shows a page that nests again "feed stats athlete" that should be shown only if you are browsing a user that is not you 2026-04-09 10:35:58 +02:00
Davide Scaini 98c42dc443 unify single user and multi user behaviour 2026-04-09 08:59:40 +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 e940338816 planning 2026-04-06 19:31:52 +02:00
Davide Scaini 81438231b4 fix low level issues 2026-03-31 23:22:12 +02:00
Davide Scaini ec6175b143 athlete page first draft 2026-03-30 09:05:18 +02:00
Davide Scaini 4537273de9 get default hr and power zones from config file 2026-03-29 22:06:22 +02:00
Davide Scaini 3441079913 map now working 2026-03-28 19:34:22 +01:00