From 395182649b136831f3323dbd3122c4d7d028a640 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 15 Apr 2026 23:07:52 +0200 Subject: [PATCH] improve docs --- .gitignore | 3 + README.md | 3 +- bincio/serve/server.py | 124 ++++++++++---- docs/admin-guide.md | 306 +++++++++++++++++++++++++++++++++ docs/architecture.md | 2 +- docs/deployment/multi-user.md | 2 +- docs/developer-guide.md | 307 ++++++++++++++++++++++++++++++++++ docs/getting-started.md | 2 +- docs/index.md | 38 +++++ SCHEMA.md => docs/schema.md | 0 docs/user-guide.md | 180 ++++++++++++++++++++ mkdocs.yml | 66 ++++++++ pyproject.toml | 4 + 13 files changed, 1004 insertions(+), 33 deletions(-) create mode 100644 docs/admin-guide.md create mode 100644 docs/developer-guide.md create mode 100644 docs/index.md rename SCHEMA.md => docs/schema.md (100%) create mode 100644 docs/user-guide.md create mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index f20ab04..fa7a52d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ site/node_modules/ site/dist/ site/.astro/ +# MkDocs +mkdocs-site/ + # BAS data stores (user data, not committed to the tool repo) bincio_data/ *.bincio_cache.json diff --git a/README.md b/README.md index 6160ab4..a2cb2f5 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Privacy is enforced at extract time. A `private` activity never enters `index.js `index.json` is everything the feed page needs — no extra fetches until you open an activity. `{id}.json` contains the full timeseries (elevation, speed, HR, cadence, power at 1 Hz) for charts and the detail map. Both are human-readable and editable with any text editor. -See [SCHEMA.md](SCHEMA.md) for the full specification. +See [SCHEMA.md](docs/schema.md) for the full specification. --- @@ -293,7 +293,6 @@ bincio/ Python package server.py FastAPI write API (activity edits, image + file upload) schema/ bas-v1.schema.json JSON Schema for BAS format -SCHEMA.md Human-readable BAS specification site/ Astro project src/ pages/ diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 7d24c8e..e486139 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -49,6 +49,73 @@ from bincio.serve.db import ( use_invite, ) +from pydantic import BaseModel, Field + +# ── Pydantic request/response models ───────────────────────────────────────── + + +class LoginRequest(BaseModel): + handle: str = Field(..., description="User handle (username)") + password: str = Field(..., description="User password") + + +class LoginResponse(BaseModel): + ok: bool = Field(True, description="Success flag") + handle: str = Field(..., description="User handle") + display_name: str = Field(..., description="User's display name") + + +class ResetPasswordRequest(BaseModel): + handle: str = Field(..., description="User handle") + code: str = Field(..., description="Reset code (24 hours valid)") + password: str = Field(..., description="New password (min 8 chars)") + + +class RegisterRequest(BaseModel): + code: str = Field(..., description="Invite code") + handle: str = Field(..., description="Desired username (lowercase, 1-30 chars)") + password: str = Field(..., description="Password (min 8 characters)") + display_name: str = Field(default="", description="Full name (optional, defaults to handle)") + + +class RegisterResponse(BaseModel): + ok: bool = Field(True, description="Success flag") + handle: str = Field(..., description="New user's handle") + + +class CurrentUserResponse(BaseModel): + handle: str = Field(..., description="User handle") + display_name: str = Field(..., description="User's display name") + is_admin: bool = Field(..., description="Whether user is an admin") + store_originals_default: bool = Field( + default=True, + description="Instance-wide default for storing original files" + ) + + +class ActivityEditRequest(BaseModel): + title: str | None = Field(default=None, description="Activity title") + description: str | None = Field(default=None, description="Activity description (markdown)") + sport: str | None = Field(default=None, description="Sport type") + private: bool | None = Field(default=None, description="Hide from public feed") + highlight: bool | None = Field(default=None, description="Mark as favorite") + gear: str | None = Field(default=None, description="Gear used (e.g., 'Trek Domane')") + + +class ActivityEditResponse(BaseModel): + ok: bool = Field(True, description="Success flag") + + +class ResetPasswordCodeResponse(BaseModel): + ok: bool = Field(True, description="Success flag") + code: str = Field(..., description="One-time reset code") + expires_in_hours: int = Field(24, description="Code validity period in hours") + + +class GenericResponse(BaseModel): + ok: bool = Field(True, description="Success flag") + + # ── Active job tracker ─────────────────────────────────────────────────────── # Tracks in-progress upload/processing jobs so admins can see what's running. # Jobs are added when a streaming upload starts and removed when it finishes. @@ -132,7 +199,7 @@ def _get_data_dir() -> Path: # ── App ─────────────────────────────────────────────────────────────────────── -app = FastAPI(title="BincioActivity Serve", docs_url=None, redoc_url=None) +app = FastAPI(title="BincioActivity Serve") @app.on_event("startup") @@ -326,7 +393,7 @@ def _trigger_rebuild(handle: str) -> None: # ── Auth endpoints ──────────────────────────────────────────────────────────── -@app.get("/api/me") +@app.get("/api/me", response_model=CurrentUserResponse) async def me(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse: user = _current_user(bincio_session) if not user: @@ -361,14 +428,16 @@ async def stats() -> JSONResponse: }) -@app.post("/api/auth/login") -async def login(request: Request) -> JSONResponse: +@app.post("/api/auth/login", response_model=LoginResponse) +async def login( + login_req: LoginRequest, + request: Request, +) -> JSONResponse: ip = request.client.host if request.client else "unknown" _check_rate_limit(ip, _login_attempts, _LOGIN_RATE_LIMIT, "Too many login attempts. Try again later.") - body = await request.json() - handle = body.get("handle", "").strip().lower() - password = body.get("password", "") + handle = login_req.handle.strip().lower() + password = login_req.password user = authenticate(_get_db(), handle, password) if not user: @@ -380,7 +449,7 @@ async def login(request: Request) -> JSONResponse: return resp -@app.post("/api/auth/logout") +@app.post("/api/auth/logout", response_model=GenericResponse) async def logout(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse: if bincio_session: delete_session(_get_db(), bincio_session) @@ -389,16 +458,13 @@ async def logout(bincio_session: Optional[str] = Cookie(default=None)) -> JSONRe return resp -@app.post("/api/auth/reset-password") -async def reset_password(request: Request) -> JSONResponse: +@app.post("/api/auth/reset-password", response_model=GenericResponse) +async def reset_password(reset_req: ResetPasswordRequest) -> JSONResponse: """Validate a reset code and set a new password. Public endpoint.""" from bincio.serve.db import use_reset_code, change_password - body = await request.json() - handle = (body.get("handle") or "").strip().lower() - code = (body.get("code") or "").strip().upper() - new_pw = body.get("password") or "" - if not handle or not code or not new_pw: - raise HTTPException(400, "handle, code, and password are required") + handle = reset_req.handle.strip().lower() + code = reset_req.code.strip().upper() + new_pw = reset_req.password if len(new_pw) < 8: raise HTTPException(400, "Password must be at least 8 characters") db = _get_db() @@ -410,16 +476,18 @@ async def reset_password(request: Request) -> JSONResponse: # ── Registration ────────────────────────────────────────────────────────────── -@app.post("/api/register") -async def register(request: Request) -> JSONResponse: +@app.post("/api/register", response_model=RegisterResponse) +async def register( + register_req: RegisterRequest, + request: Request, +) -> JSONResponse: ip = request.client.host if request.client else "unknown" _check_rate_limit(ip, _register_attempts, _REGISTER_RATE_LIMIT, "Too many registration attempts. Try again later.") - body = await request.json() - code = body.get("code", "").strip().upper() - handle = body.get("handle", "").strip().lower() - password = body.get("password", "") - display = body.get("display_name", "").strip() or handle + code = register_req.code.strip().upper() + handle = register_req.handle.strip().lower() + password = register_req.password + display = register_req.display_name.strip() or handle if not _VALID_HANDLE.match(handle): raise HTTPException(400, "Invalid handle (lowercase letters, numbers, _ - only; max 30 chars)") @@ -568,7 +636,7 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS }) -@app.post("/api/admin/users/{handle}/reset-password-code") +@app.post("/api/admin/users/{handle}/reset-password-code", response_model=ResetPasswordCodeResponse) async def admin_reset_password_code( handle: str, bincio_session: Optional[str] = Cookie(default=None), @@ -1171,10 +1239,10 @@ async def get_activity( return JSONResponse(json.loads(path.read_text())) -@app.post("/api/activity/{activity_id}") +@app.post("/api/activity/{activity_id}", response_model=ActivityEditResponse) async def post_activity( activity_id: str, - request: Request, + edit_req: ActivityEditRequest, bincio_session: Optional[str] = Cookie(default=None), ) -> JSONResponse: user = _require_user(bincio_session) @@ -1185,13 +1253,13 @@ async def post_activity( raise HTTPException(404, "Activity not found") from bincio.edit.ops import apply_sidecar_edit - body = await request.json() + body = edit_req.model_dump(exclude_none=True) # apply_sidecar_edit already calls merge_one internally — no full rebuild needed. apply_sidecar_edit(activity_id, body, dd) return JSONResponse({"ok": True}) -@app.delete("/api/activity/{activity_id}") +@app.delete("/api/activity/{activity_id}", response_model=GenericResponse) async def delete_activity( activity_id: str, bincio_session: Optional[str] = Cookie(default=None), diff --git a/docs/admin-guide.md b/docs/admin-guide.md new file mode 100644 index 0000000..f72588d --- /dev/null +++ b/docs/admin-guide.md @@ -0,0 +1,306 @@ +# Administrator Guide + +This guide covers everything needed to deploy and maintain a multi-user BincioActivity instance. + +## Before You Start + +**[Multi-user Deployment](deployment/multi-user.md)** has the complete step-by-step instructions. This guide focuses on day-to-day admin tasks once the instance is running. + +## Initializing an Instance + +```bash +uv sync --extra serve + +uv run bincio init \ + --data-dir /var/bincio \ + --handle your_admin_handle \ + --display-name "Your Name" \ + --name "Instance Name" +``` + +You'll be prompted for a password. This creates: + +- `/var/bincio/instance.db` — SQLite database (users, sessions, invites, reset codes) +- `/var/bincio/index.json` — root shard manifest (`"private": true` by default) +- Your admin user account +- A first invite code + +`bincio init` is idempotent — safe to re-run. + +Optional flags: + +- `--max-users N` — limit total registered users (0 or omitted = unlimited) +- `--store-originals false` — don't keep uploaded source files (defaults to true) + +## Inviting Users + +### Generate an invite code (as admin) + +From the web UI at `/invites/` (requires login as admin), or via CLI: + +```bash +uv run python -c " +from pathlib import Path +from bincio.serve.db import open_db, create_invite +db = open_db(Path('/var/bincio')) +code = create_invite(db, 'your_handle') +print(f'https://yourdomain.com/register/?code={code}') +" +``` + +### Invite limits + +- **Admins:** unlimited invites +- **Regular users:** 3 invites each (configurable in `bincio/serve/db.py` as `_MAX_USER_INVITES`) + +### Share the invite link + +Send the registration link to the user: + +``` +https://yourdomain.com/register/?code=ABCD1234 +``` + +They create their own handle and password. After registration, they can: +- Upload activity files (GPX, FIT, TCX) +- Sync from Strava +- Edit activity titles, descriptions, photos +- Control privacy per activity + +## Password Reset + +BincioActivity has no email system. Password resets work via **admin-generated one-time codes**. + +### Reset a user's password (as admin) + +1. Open `/admin/` in the web UI (must be logged in as admin) +2. Find the user and click **Reset password** +3. A code appears (monospace, click to copy) +4. Send the code out-of-band (Signal, Telegram, WhatsApp, etc.) + +The code is valid for **24 hours**. Users reset their password at `/reset-password/` by entering: + +- Their **handle** +- The **code** +- Their **new password** + +### Reset code API (CLI) + +To generate a reset code programmatically: + +```bash +uv run python -c " +from pathlib import Path +from bincio.serve.db import open_db, create_reset_code +db = open_db(Path('/var/bincio')) +code, expires_in_hours = create_reset_code(db, 'user_handle', 'your_handle') +print(f'Code: {code} (expires in {expires_in_hours} hours)') +" +``` + +## Monitoring Active Jobs + +The `/api/admin/jobs` endpoint (admin-only) shows which uploads/syncs are in progress: + +```bash +curl -b "bincio_session=$(cat /tmp/session.txt)" http://localhost:4041/api/admin/jobs +``` + +Returns: + +```json +[ + { + "id": "a1b2c3d4", + "user": "alice", + "started_at": 1712345678, + "total": 50, + "done": 23, + "current": "activity_2026-03-15_120000Z.fit" + } +] +``` + +## Triggering Rebuilds + +`bincio serve` can trigger incremental rebuilds when you pass `--site-dir`: + +```bash +uv run bincio serve \ + --data-dir /var/bincio \ + --site-dir /var/www/bincio/src/site +``` + +After any write operation (edit, upload, Strava sync), the affected user's shard is rebuilt automatically and the static site is updated. + +To manually rebuild a single user's shard: + +```bash +uv run bincio render \ + --data-dir /var/bincio \ + --handle alice +``` + +To rebuild everything (slow): + +```bash +uv run bincio render --data-dir /var/bincio +``` + +## Instance Settings + +Settings are stored in `instance.db` and control instance-wide behavior: + +| Setting | Default | Controls | +|---------|---------|----------| +| `max_users` | unlimited | Maximum registered users allowed | +| `store_originals` | `true` | Keep uploaded source files and Strava sync data | + +Read/set settings via CLI: + +```bash +uv run python -c " +from pathlib import Path +from bincio.serve.db import open_db, get_setting, set_setting +db = open_db(Path('/var/bincio')) +print(get_setting(db, 'max_users')) +set_setting(db, 'max_users', 100) +db.commit() +" +``` + +Or check the database directly: + +```bash +sqlite3 /var/bincio/instance.db +> SELECT key, value FROM settings; +``` + +## Instance Privacy + +By default, new instances are **private** — only authenticated users can view anything. Edit the root `index.json` to toggle: + +```json +{ + "private": false, + "shards": [...] +} +``` + +- **`"private": true`** — all pages (except login/register) require authentication +- **`"private": false`** — public access to all activities; individual activities can still be marked private via the `private` flag in sidecars + +After any change, run `bincio render` to apply it: + +```bash +uv run bincio render --data-dir /var/bincio +``` + +## Data Directory Layout + +``` +/var/bincio/ + instance.db ← SQLite: users, sessions, invites, reset codes + index.json ← root shard manifest + {handle}/ + index.json ← user's BAS feed (activities list) + _merged/ ← sidecar-merged output (served to browser) + activities/ ← extracted activity JSON files + {id}.json + ... + edits/ ← user-made sidecar edits + {id}.md + images/{id}/ + athlete.json ← profile (from Strava or manual) + strava_token.json ← OAuth token (if synced from Strava) + originals/ ← source files (if store_originals=true) + _feedback/ ← user feedback submissions + {handle}.json + {handle}/ + {timestamp}_{id}_{filename} +``` + +## Database Schema + +`instance.db` contains: + +- **`users`** — handle, password hash, display_name, is_admin, created_at +- **`sessions`** — session_id, handle, created_at, expires_at +- **`invites`** — code, created_by, created_at, used_by, used_at +- **`reset_codes`** — code, handle, created_by, created_at, expires_at, used_at +- **`settings`** — key, value (instance config) +- **`user_preferences`** — handle, key, value (per-user settings) + +Query the database directly: + +```bash +sqlite3 /var/bincio/instance.db ".tables" +sqlite3 /var/bincio/instance.db "SELECT handle, is_admin FROM users;" +``` + +## API Endpoints for Admins + +The `/api/admin/*` endpoints require authentication and admin privileges: + +- `GET /api/admin/users` — List all users +- `POST /api/admin/users/{handle}/reset-password-code` — Generate a reset code +- `GET /api/admin/jobs` — Show active uploads/syncs +- `GET /api/stats` — Community stats (public) + +See [API Reference](reference/api.md) for full details. + +### Explore the API with Swagger UI + +When `bincio serve` is running, visit `/api/docs` to see an interactive Swagger UI. You can: +- Browse all endpoints with their parameters and response types +- Try out requests directly (if you're logged in as admin) +- See live examples of request/response bodies + +ReDoc (another API documentation format) is also available at `/api/redoc` with a different UI. + +## Running as a systemd service + +See [Multi-user Deployment](deployment/multi-user.md#step-5--start-bincio-serve) for the systemd unit file. Key points: + +- Set `User=bincio` (unprivileged user) +- Set `WorkingDirectory` to the repo root +- Use `--site-dir` to enable incremental rebuilds +- Restart policy: `Restart=on-failure` + +Monitor with: + +```bash +systemctl status bincio +journalctl -u bincio -f +``` + +## Troubleshooting + +### Activities not appearing after upload + +1. Check if the job is still running: `GET /api/admin/jobs` +2. Check logs: `journalctl -u bincio -f` +3. If `store_originals=true`, verify the source file is readable in `{handle}/originals/` +4. Re-trigger the merge: `uv run bincio render --data-dir /var/bincio --handle alice` + +### Database locked + +If you see "database is locked": + +1. Verify no other `bincio` processes are running: `ps aux | grep bincio` +2. Kill any stuck processes: `pkill -f 'uv run bincio'` +3. Restart the service: `systemctl restart bincio` + +### High memory usage + +The first rebuild on a large instance can be memory-intensive. Consider: + +- Running `bincio render` during off-hours +- Rebuilding one user at a time: `uv run bincio render --data-dir /var/bincio --handle alice` +- Increasing swap or upgrading the machine + +## See also + +- [Multi-user Deployment](deployment/multi-user.md) — complete step-by-step setup +- [Single-user Deployment](deployment/single-user.md) — if you're hosting a read-only site +- [API Reference](reference/api.md) — all HTTP endpoints diff --git a/docs/architecture.md b/docs/architecture.md index d90a3e3..d8974ea 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,7 +21,7 @@ GPX / FIT / TCX files Any static host (GitHub Pages, Netlify, VPS, USB stick, …) ``` -The BAS data store is the contract between the two stages. Any tool in any language can produce BAS-compliant JSON. See [SCHEMA.md](../SCHEMA.md) for the format. +The BAS data store is the contract between the two stages. Any tool in any language can produce BAS-compliant JSON. See [SCHEMA.md](schema.md) for the format. --- diff --git a/docs/deployment/multi-user.md b/docs/deployment/multi-user.md index 3ddae04..373d139 100644 --- a/docs/deployment/multi-user.md +++ b/docs/deployment/multi-user.md @@ -204,4 +204,4 @@ The browser fetches and merges remote shards concurrently. Remote activities app - [CLI reference — bincio serve](../reference/cli.md#bincio-serve) - [CLI reference — bincio dev](../reference/cli.md#bincio-dev) - [API reference](../reference/api.md) -- [BAS schema — instance manifest](../../SCHEMA.md#instance-manifest) +- [BAS schema — instance manifest](../schema.md#instance-manifest) diff --git a/docs/developer-guide.md b/docs/developer-guide.md new file mode 100644 index 0000000..8273931 --- /dev/null +++ b/docs/developer-guide.md @@ -0,0 +1,307 @@ +# Developer Guide + +This guide is for developers contributing to BincioActivity. + +## Prerequisites + +- **Python 3.12+** with [uv](https://docs.astral.sh/uv/) +- **Node 20+** with npm +- **Git** + +## Local Setup + +```bash +git clone https://github.com/brutsalvadi/bincio-activity.git +cd bincio-activity + +# Install Python dependencies +uv sync + +# Install optional extras for multi-user development +uv sync --extra serve --extra edit + +# Install Node dependencies (for the site) +cd site && npm install && cd .. +``` + +## Running Locally + +### Single-user (fastest for testing extract logic) + +```bash +# Configure where to find your test activities +cp extract_config.example.yaml extract_config.yaml +$EDITOR extract_config.yaml # set input.dirs and output.dir + +# Extract activities +uv run bincio extract + +# Start the dev server (no login, no API server) +uv run bincio dev --data-dir ~/bincio_data +# → http://localhost:4321/u/{handle}/ +``` + +### Multi-user (for testing auth, write API, admin features) + +```bash +# Create a test instance with an admin user +uv run bincio init --data-dir /tmp/bincio_test --handle testadmin + +# Extract activities +uv run bincio extract --output /tmp/bincio_test + +# Start everything (bincio serve + astro dev) +uv run bincio dev --data-dir /tmp/bincio_test +# → http://localhost:4321 (login with testadmin/{password}) +``` + +Ctrl+C stops both servers. + +## Running Tests + +```bash +# All tests +uv run pytest + +# Specific test file +uv run pytest tests/extract/test_parsers.py + +# Specific test function +uv run pytest tests/extract/test_parsers.py::test_gpx_parser + +# With verbose output +uv run pytest -vv + +# With coverage report +uv run pytest --cov=bincio +``` + +Tests are in `tests/` and use pytest + fixtures for DRY test data. + +## Project Structure + +``` +bincio/ + extract/ Python package for GPX/FIT/TCX parsing + models.py DataPoint, ParsedActivity, LapData + parsers/ GPX, FIT, TCX parsers + factory + sport.py Sport name normalization + metrics.py Haversine-based stats (distance, elevation) + timeseries.py 1Hz downsampling → BAS timeseries object + simplify.py RDP track simplification (no external deps) + dedup.py Exact + fuzzy duplicate detection + strava_csv.py Strava activities.csv importer + writer.py BAS JSON + GeoJSON output + config.py extract_config.yaml loader + cli.py `bincio extract` command + render/ + cli.py `bincio render` command + merge.py Sidecar edit overlay (produces _merged/) + edit/ + cli.py `bincio edit` FastAPI server + server.py Edit API endpoints + serve/ + cli.py `bincio serve` command + server.py Multi-user FastAPI server (auth, invites, admin) + db.py SQLite data layer + init_cmd.py `bincio init` bootstrap + shared/ (if needed) + +site/ Astro + Svelte + Tailwind frontend + src/ + layouts/ Base.astro (auth wall, nav) + pages/ Routes (activity feed, detail, login, etc.) + components/ Svelte components (maps, charts, edit drawer) + lib/ TypeScript utilities (types, format, dataloader) + +tests/ pytest test suite + extract/ + render/ + serve/ + fixtures/ Shared test data +``` + +## Key Concepts + +### BAS (BincioActivity Schema) + +Activity data flows as **BAS JSON** files in `{user}/activities/`. The format is specified in [SCHEMA.md](schema.md). + +Key files: + +- `{id}.json` — activity metadata + timeseries +- `_merged/` symlink — sidecar edits overlaid on activities +- `edits/{id}.md` — user-created sidecar (optional) + +### Shard model + +Multi-user instances use a **shard manifest** (root `index.json`) that lists per-user shards. The browser fetches all shards concurrently and merges them. This allows: + +- Federation (remote shard URLs) +- Yearly pagination +- No data duplication + +### Extract pipeline + +``` +GPX/FIT/TCX files + ↓ (parse) +ParsedActivity + ↓ (calculate metrics) +BAS Activity JSON + ↓ (downsample to 1Hz) +Timeseries + ↓ (simplify with RDP) +GeoJSON + ↓ (write) +activities/{id}.json + activities/{id}.geojson +``` + +### Render pipeline + +``` +{user}/ + activities/*.json (extracted) + edits/*.md (user sidecars) + ↓ (merge_all) +_merged/ + index.json (sidecar edits applied) + activities/{id}.json + {id}.geojson + ↓ (astro build) +site/dist/ +``` + +Editing does not require re-extraction. + +## Making Changes + +### Adding a new endpoint + +1. Add a route in `bincio/serve/server.py` (or `bincio/edit/server.py` for single-user) +2. Add Pydantic models for request/response if needed +3. Add tests in `tests/serve/` +4. Update `docs/reference/api.md` with the new endpoint +5. If admin-only, protect it with `await _require_admin(bincio_session)` + +### Adding a parser for a new format + +1. Create `bincio/extract/parsers/myformat.py` +2. Implement a parser class with `parse(file_path: Path) -> ParsedActivity` +3. Register it in `bincio/extract/parsers/__init__.py` +4. Add tests in `tests/extract/test_parsers.py` + +### Modifying BAS schema + +1. Edit `schema/bas-v1.schema.json` (JSON Schema) +2. Update `SCHEMA.md` (human-readable spec) +3. Update TypeScript types in `site/src/lib/types.ts` +4. Add a migration if the change is breaking + +### Frontend changes + +**Svelte components** are in `site/src/components/`. Key ones: + +- `ActivityFeed.svelte` — activity grid + filters +- `ActivityDetail.svelte` — activity page (maps, charts, photos) +- `EditDrawer.svelte` — sidecar editor + +Use `uv run bincio dev` to test changes live. The site hot-reloads on file changes. + +## Code Style + +- **Python:** PEP 8, type hints where possible +- **JavaScript/TypeScript:** ESLint + Prettier (configured in `site/`) +- **Svelte:** No self-closing non-void tags; interactive divs need `role` + keyboard handler + +## Git Workflow + +1. Create a branch: `git checkout -b feature/my-feature` +2. Make changes and test locally +3. Commit: `git commit -m "Clear, specific commit message"` +4. Push: `git push origin feature/my-feature` +5. Open a pull request + +**Commit message style:** + +- Imperative mood ("add feature", not "added feature") +- Reference issues if relevant: "fix #123" +- First line ≤ 50 characters +- Blank line, then detailed explanation if needed + +## Performance Considerations + +### Extract speed + +- **ProcessPoolExecutor with initializer** — large data (Strava lookups, hash sets) is sent once per worker, not per task +- **Haversine** — 10x faster than geopy for distance calculations +- **Lazy parsing** — FIT files decoded only once per task + +### Render speed + +- **RDP simplification** — custom implementation (no external wheels for Pyodide) +- **Gzip compression** — activity JSON and geojson are served gzipped +- **Concurrent shard fetch** — browser loads all shards in parallel + +### Frontend + +- **MapLibre GL v5** — requires explicit center/zoom and workarounds +- **Observable Plot** — use hyphenated curve names (e.g. `"monotone-x"`) +- **Client-only for complex components** — use `client:only="svelte"` for activity detail to avoid hydration mismatches + +## Debugging + +### Python + +```bash +# Interactive debugger +uv run python -m pdb -m bincio.extract.cli + +# Or use breakpoint() in code +breakpoint() +uv run bincio extract +``` + +### TypeScript + +Check your editor's TypeScript integration. The site has strict `tsconfig.json`. + +### Frontend + +- Open DevTools (F12) +- Check the Network tab for API calls +- Check Console for client-side errors + +### Database + +```bash +# Inspect the SQLite database directly +sqlite3 /tmp/bincio_test/instance.db +> SELECT * FROM users; +``` + +## Documentation + +- User-facing docs go in `docs/` +- API docs are auto-generated from FastAPI routes (and should be typed with Pydantic models) +- Code comments should explain *why*, not *what* + +## Known Issues & Limitations + +See the [GitHub repository](https://github.com/brutsalvadi/bincio-activity) for known issues and planned features. + +## Contributing + +Contributions are welcome! Please: + +1. Check existing issues/PRs so you're not duplicating work +2. Open an issue first for large changes +3. Include tests for new features +4. Update docs (user guide, API ref, or developer guide) +5. Follow the code style guidelines + +## See also + +- [Architecture](architecture.md) — system design and data flow +- [BAS Schema](schema.md) — activity data format +- [API Reference](reference/api.md) — all HTTP endpoints diff --git a/docs/getting-started.md b/docs/getting-started.md index 6f2d7b2..28062a3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -117,4 +117,4 @@ In multi-user mode the edit UI is always available via `bincio serve` — no ext - [Single-user deployment](deployment/single-user.md) — GitHub Pages, Netlify, VPS - [Multi-user deployment](deployment/multi-user.md) — VPS with nginx, inviting users - [CLI reference](reference/cli.md) — all commands and options -- [BAS schema](../SCHEMA.md) — the data format and federation protocol +- [BAS schema](schema.md) — the data format and federation protocol diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6a2736b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,38 @@ +# BincioActivity Documentation + +Welcome to BincioActivity — a federated, self-hosted activity stats platform. This documentation is organized by audience: + +## For Users + +**[Getting Started](getting-started.md)** — Extract your activities from Strava/Garmin, set up a local site, and deploy it. + +**[User Guide](user-guide.md)** — Upload activities, sync from Strava, edit titles/descriptions, manage photos, control privacy, configure your profile. + +## For Administrators + +**[Admin Guide](admin-guide.md)** — Deploy a multi-user instance, manage users, reset passwords, monitor rebuild status. + +**[Multi-user Deployment](deployment/multi-user.md)** — Step-by-step setup with nginx, systemd, and multi-user architecture. + +**[Single-user Deployment](deployment/single-user.md)** — Deploy as a read-only static site or with a local edit server. + +## For Developers + +**[Developer Guide](developer-guide.md)** — Local setup, how to run tests, architecture overview, how to contribute. + +**[Architecture](architecture.md)** — BAS data format, shard model, federation protocol, federation design. + +**[API Reference](reference/api.md)** — HTTP endpoints, request/response formats, authentication, rate limits. + +**[CLI Reference](reference/cli.md)** — All bincio commands and options. + +## Quick Links + +- [GitHub repo](https://github.com/brutsalvadi/bincio-activity) +- [BAS Schema](schema.md) — The data format specification +- [Architecture diagram](architecture.mmd) (Mermaid diagram) +- Live Swagger UI at `/api/docs` (when server is running) + +--- + +**Status:** This is early-stage, self-hosted software. See the [GitHub repo](https://github.com/brutsalvadi/bincio-activity) for known issues and planned features. diff --git a/SCHEMA.md b/docs/schema.md similarity index 100% rename from SCHEMA.md rename to docs/schema.md diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..ec2de54 --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,180 @@ +# User Guide + +This guide covers everything you can do as a BincioActivity user. + +## Getting Your Account + +Your instance administrator sends you a registration link: + +``` +https://yourdomain.com/register/?code=ABCD1234 +``` + +Click it and create: + +- **Handle** — your username in URLs (lowercase letters, numbers, `_`, `-`; 1–30 chars) +- **Password** — at least 8 characters +- **Display name** — your full name (shown on your profile page) + +You're now logged in and ready to upload activities! + +## Uploading Activities + +Click **Upload** to add activities from files. + +### Supported formats + +- **GPX** — GPS Exchange format (most common) +- **FIT** — Garmin's native format +- **TCX** — Training Center XML +- **Compressed files** — `.gz` variants of any format above + +### Using Strava export + +If you exported activities from Strava, you likely have a folder like: + +``` +activities/ + ├── 2026-03-15_morning_run.gpx + ├── 2026-03-14_evening_ride.fit + └── ... +``` + +Just drag the whole `activities/` folder into the upload box, or select multiple files at once. + +### Upload options + +- **Store original files** — keep the source GPX/FIT/TCX file on the server (checked by default; you can uncheck per upload) +- **Skip duplicates** — the system detects exact duplicates automatically + +After upload, the server extracts GPS tracks, calculates distance/elevation/time, and generates your activity feed. You can keep uploading — the system deduplicates by file hash. + +## Syncing from Strava + +If your instance supports Strava sync, click **Sync from Strava** in the upload modal. + +1. Authorize BincioActivity to read your Strava data +2. Select which activities to import +3. The server fetches GPS and metrics from Strava and stores them + +Your OAuth token is stored securely on the server. You can revoke access at any time in [Strava Settings](https://www.strava.com/settings/apps). + +## Editing Activities + +Click **Edit** on any activity to: + +- **Change the title** — rename the activity +- **Add a description** — write notes or a story (supports markdown and embedded images) +- **Upload photos** — add photos taken during the activity +- **Choose sport type** — cycling, running, hiking, etc. +- **Assign gear** — tag the bike/shoes/watch used +- **Set privacy** — hide the activity from the public feed +- **Highlight** — mark your favorite activities + +Changes save instantly. The site rebuilds in the background. + +### Photo gallery + +Upload photos for an activity. They appear in a lightbox on the activity detail page. The server stores them in your data directory. + +### Markdown in descriptions + +Descriptions support basic markdown: + +```markdown +# Title +**bold** _italic_ `code` + +- bullet list +- another item + +[link](https://example.com) + +![image name](image.jpg) +``` + +Images are stored in `edits/images/{id}/` and paths are rewritten automatically. + +## Privacy Control + +Each activity has a privacy setting: + +- **Public** (`public: true`) — visible to all logged-in users in the feed +- **Unlisted** (`private: true`) — not shown in the feed, but accessible by direct URL (for sharing) +- **No GPS** (remove GPS track) — hides the map but keeps distance/time stats + +Your instance admin can also make the whole instance public or private. + +### Deleting an activity + +You can't delete an activity directly, but you can: + +- Mark it **private** to hide it from the feed +- Edit the sidecar manually in `{data-root}/edits/{id}.md` and delete the file + +## Your Profile + +Click your name in the top-right to view your profile at `/u/{handle}/`. It shows: + +- Your display name +- All your public activities (organized by year) +- Summary stats (total distance, time, elevation) + +## Account Settings + +Click your name → **Settings** to: + +- **Change password** — update your account password +- **View your handle** — the username used in URLs +- **See your data** — information about what's stored on the server + +If you forget your password, ask your instance administrator to generate a reset code. + +## Feedback + +Found a bug or want to suggest a feature? Click **Feedback** at the bottom of any page to submit a message and optional screenshots. The admin team can see all feedback submissions. + +## Local Activity Conversion + +If your instance has the `/convert/` page enabled, you can: + +1. Upload a GPX/FIT/TCX file **locally in your browser** (no server upload) +2. The file is processed in JavaScript (powered by Pyodide, Python in the browser) +3. You see the activity preview immediately +4. You can then save it to your local browser storage (IndexedDB) or upload it to the server + +This is useful for testing or converting files without uploading them first. + +## Offline Activity Storage (experimental) + +Activities converted locally are stored in your browser's **IndexedDB** (local storage). They: + +- Don't upload to the server +- Persist across browser sessions +- Can be deleted from settings + +This is useful for activities you don't want to publish yet, or for testing before uploading. + +## Frequently Asked Questions + +**Can I download my data?** +Your instance's complete activity feed is at `/u/{handle}/index.json` (the BAS format). You can also ask the admin to copy your data directory directly. + +**Can I transfer activities between instances?** +Yes! Copy the `{handle}/activities/` and `{handle}/edits/` directories to another instance. The system uses content hashing, so you can merge multiple instances. + +**What formats does my activity support?** +BincioActivity extracts GPS tracks, distance, elevation, moving time, average speed, heart rate, power, cadence, and temperature (if available in the source file). + +**Can I share my activities with someone outside my instance?** +Mark activities as **unlisted** (`private: true`). Anyone with the direct URL can view them, even if they're not logged in. + +**How do I delete my account?** +Ask your instance administrator. They can delete your user record from `instance.db`, which removes you from the login system. Your activity data remains for audit, but can be deleted from disk if you request it. + +## See also + +- [Getting Started](getting-started.md) — initial setup +- [API Reference](reference/api.md) — technical details about how data flows +- [BAS Schema](schema.md) — the activity JSON format +- [Admin Guide](admin-guide.md) — for instance admins diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..b443f55 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,66 @@ +site_name: BincioActivity +site_description: Federated, open-source, self-hosted activity stats platform +site_author: Davide Brugali +repo_url: https://github.com/brutsalvadi/bincio-activity +repo_name: brutsalvadi/bincio-activity +edit_uri: edit/main/docs/ + +docs_dir: docs +site_dir: mkdocs-site + +theme: + name: material + palette: + - scheme: light + primary: blue + accent: blue + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + - scheme: slate + primary: blue + accent: blue + toggle: + icon: material/lightbulb + name: Switch to light mode + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + +plugins: + - search + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + use_pygments: true + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + +nav: + - Home: index.md + - Getting Started: getting-started.md + - User Guide: user-guide.md + - Admin Guide: admin-guide.md + - Deployment: + - Single-user: deployment/single-user.md + - Multi-user: deployment/multi-user.md + - VPS: deployment/vps.md + - Developer: + - Developer Guide: developer-guide.md + - Architecture: architecture.md + - Reference: + - API: reference/api.md + - CLI: reference/cli.md + - Schema: schema.md + - Garmin Disclaimer: garmin_connect_disclaimer.md diff --git a/pyproject.toml b/pyproject.toml index 2e1bb5b..e251829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,9 @@ dev = [ "types-pyyaml", "types-jsonschema", ] +docs = [ + "mkdocs-material>=9.5", +] [project.scripts] bincio = "bincio.cli:main" @@ -68,6 +71,7 @@ dev = [ "mypy>=1.11", "types-pyyaml", "types-jsonschema", + "mkdocs-material>=9.5", ] [tool.ruff]