improve docs

This commit is contained in:
Davide Scaini
2026-04-15 23:07:52 +02:00
parent bfb6432666
commit 395182649b
13 changed files with 1004 additions and 33 deletions
+3
View File
@@ -21,6 +21,9 @@ site/node_modules/
site/dist/ site/dist/
site/.astro/ site/.astro/
# MkDocs
mkdocs-site/
# BAS data stores (user data, not committed to the tool repo) # BAS data stores (user data, not committed to the tool repo)
bincio_data/ bincio_data/
*.bincio_cache.json *.bincio_cache.json
+1 -2
View File
@@ -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. `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) server.py FastAPI write API (activity edits, image + file upload)
schema/ schema/
bas-v1.schema.json JSON Schema for BAS format bas-v1.schema.json JSON Schema for BAS format
SCHEMA.md Human-readable BAS specification
site/ Astro project site/ Astro project
src/ src/
pages/ pages/
+96 -28
View File
@@ -49,6 +49,73 @@ from bincio.serve.db import (
use_invite, 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 ─────────────────────────────────────────────────────── # ── Active job tracker ───────────────────────────────────────────────────────
# Tracks in-progress upload/processing jobs so admins can see what's running. # 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. # Jobs are added when a streaming upload starts and removed when it finishes.
@@ -132,7 +199,7 @@ def _get_data_dir() -> Path:
# ── App ─────────────────────────────────────────────────────────────────────── # ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(title="BincioActivity Serve", docs_url=None, redoc_url=None) app = FastAPI(title="BincioActivity Serve")
@app.on_event("startup") @app.on_event("startup")
@@ -326,7 +393,7 @@ def _trigger_rebuild(handle: str) -> None:
# ── Auth endpoints ──────────────────────────────────────────────────────────── # ── Auth endpoints ────────────────────────────────────────────────────────────
@app.get("/api/me") @app.get("/api/me", response_model=CurrentUserResponse)
async def me(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse: async def me(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
user = _current_user(bincio_session) user = _current_user(bincio_session)
if not user: if not user:
@@ -361,14 +428,16 @@ async def stats() -> JSONResponse:
}) })
@app.post("/api/auth/login") @app.post("/api/auth/login", response_model=LoginResponse)
async def login(request: Request) -> JSONResponse: async def login(
login_req: LoginRequest,
request: Request,
) -> JSONResponse:
ip = request.client.host if request.client else "unknown" 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.") _check_rate_limit(ip, _login_attempts, _LOGIN_RATE_LIMIT, "Too many login attempts. Try again later.")
body = await request.json() handle = login_req.handle.strip().lower()
handle = body.get("handle", "").strip().lower() password = login_req.password
password = body.get("password", "")
user = authenticate(_get_db(), handle, password) user = authenticate(_get_db(), handle, password)
if not user: if not user:
@@ -380,7 +449,7 @@ async def login(request: Request) -> JSONResponse:
return resp 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: async def logout(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
if bincio_session: if bincio_session:
delete_session(_get_db(), 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 return resp
@app.post("/api/auth/reset-password") @app.post("/api/auth/reset-password", response_model=GenericResponse)
async def reset_password(request: Request) -> JSONResponse: async def reset_password(reset_req: ResetPasswordRequest) -> JSONResponse:
"""Validate a reset code and set a new password. Public endpoint.""" """Validate a reset code and set a new password. Public endpoint."""
from bincio.serve.db import use_reset_code, change_password from bincio.serve.db import use_reset_code, change_password
body = await request.json() handle = reset_req.handle.strip().lower()
handle = (body.get("handle") or "").strip().lower() code = reset_req.code.strip().upper()
code = (body.get("code") or "").strip().upper() new_pw = reset_req.password
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")
if len(new_pw) < 8: if len(new_pw) < 8:
raise HTTPException(400, "Password must be at least 8 characters") raise HTTPException(400, "Password must be at least 8 characters")
db = _get_db() db = _get_db()
@@ -410,16 +476,18 @@ async def reset_password(request: Request) -> JSONResponse:
# ── Registration ────────────────────────────────────────────────────────────── # ── Registration ──────────────────────────────────────────────────────────────
@app.post("/api/register") @app.post("/api/register", response_model=RegisterResponse)
async def register(request: Request) -> JSONResponse: async def register(
register_req: RegisterRequest,
request: Request,
) -> JSONResponse:
ip = request.client.host if request.client else "unknown" 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.") _check_rate_limit(ip, _register_attempts, _REGISTER_RATE_LIMIT, "Too many registration attempts. Try again later.")
body = await request.json() code = register_req.code.strip().upper()
code = body.get("code", "").strip().upper() handle = register_req.handle.strip().lower()
handle = body.get("handle", "").strip().lower() password = register_req.password
password = body.get("password", "") display = register_req.display_name.strip() or handle
display = body.get("display_name", "").strip() or handle
if not _VALID_HANDLE.match(handle): if not _VALID_HANDLE.match(handle):
raise HTTPException(400, "Invalid handle (lowercase letters, numbers, _ - only; max 30 chars)") 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( async def admin_reset_password_code(
handle: str, handle: str,
bincio_session: Optional[str] = Cookie(default=None), bincio_session: Optional[str] = Cookie(default=None),
@@ -1171,10 +1239,10 @@ async def get_activity(
return JSONResponse(json.loads(path.read_text())) 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( async def post_activity(
activity_id: str, activity_id: str,
request: Request, edit_req: ActivityEditRequest,
bincio_session: Optional[str] = Cookie(default=None), bincio_session: Optional[str] = Cookie(default=None),
) -> JSONResponse: ) -> JSONResponse:
user = _require_user(bincio_session) user = _require_user(bincio_session)
@@ -1185,13 +1253,13 @@ async def post_activity(
raise HTTPException(404, "Activity not found") raise HTTPException(404, "Activity not found")
from bincio.edit.ops import apply_sidecar_edit 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 already calls merge_one internally — no full rebuild needed.
apply_sidecar_edit(activity_id, body, dd) apply_sidecar_edit(activity_id, body, dd)
return JSONResponse({"ok": True}) return JSONResponse({"ok": True})
@app.delete("/api/activity/{activity_id}") @app.delete("/api/activity/{activity_id}", response_model=GenericResponse)
async def delete_activity( async def delete_activity(
activity_id: str, activity_id: str,
bincio_session: Optional[str] = Cookie(default=None), bincio_session: Optional[str] = Cookie(default=None),
+306
View File
@@ -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
+1 -1
View File
@@ -21,7 +21,7 @@ GPX / FIT / TCX files
Any static host (GitHub Pages, Netlify, VPS, USB stick, …) 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.
--- ---
+1 -1
View File
@@ -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 serve](../reference/cli.md#bincio-serve)
- [CLI reference — bincio dev](../reference/cli.md#bincio-dev) - [CLI reference — bincio dev](../reference/cli.md#bincio-dev)
- [API reference](../reference/api.md) - [API reference](../reference/api.md)
- [BAS schema — instance manifest](../../SCHEMA.md#instance-manifest) - [BAS schema — instance manifest](../schema.md#instance-manifest)
+307
View File
@@ -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
+1 -1
View File
@@ -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 - [Single-user deployment](deployment/single-user.md) — GitHub Pages, Netlify, VPS
- [Multi-user deployment](deployment/multi-user.md) — VPS with nginx, inviting users - [Multi-user deployment](deployment/multi-user.md) — VPS with nginx, inviting users
- [CLI reference](reference/cli.md) — all commands and options - [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
+38
View File
@@ -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.
View File
+180
View File
@@ -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, `_`, `-`; 130 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
+66
View File
@@ -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
+4
View File
@@ -56,6 +56,9 @@ dev = [
"types-pyyaml", "types-pyyaml",
"types-jsonschema", "types-jsonschema",
] ]
docs = [
"mkdocs-material>=9.5",
]
[project.scripts] [project.scripts]
bincio = "bincio.cli:main" bincio = "bincio.cli:main"
@@ -68,6 +71,7 @@ dev = [
"mypy>=1.11", "mypy>=1.11",
"types-pyyaml", "types-pyyaml",
"types-jsonschema", "types-jsonschema",
"mkdocs-material>=9.5",
] ]
[tool.ruff] [tool.ruff]