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
+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, …)
```
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 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)
+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
- [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
+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.
+424
View File
@@ -0,0 +1,424 @@
# BincioActivity Schema (BAS) — v1.0
The BincioActivity Schema defines how activity data is stored and shared as
plain JSON files. It is the **federation protocol**: if you publish a
BAS-compliant data store, any BincioActivity instance can read it.
Any tool — in any language — can produce BAS-compliant JSON without using the
`bincio` Python package. The schema is the contract; the package is one
implementation.
---
## Files
A BAS data store is a directory (or URL prefix) with this structure:
```
{store_root}/
index.json ← user manifest and activity feed
index_{year}.json ← optional yearly shards (large datasets)
activities/
{id}.json ← full activity detail
{id}.geojson ← simplified GPS track (optional)
```
All files are UTF-8 JSON. All timestamps are ISO 8601 with timezone offset.
All distances are in metres. All speeds are in km/h. All durations are in
seconds. `null` means "not recorded / not available".
---
## `index.json`
The entry point for a data store.
```json
{
"bas_version": "1.0",
"owner": {
"handle": "brutsalvadi",
"display_name": "Bru",
"avatar_url": null
},
"generated_at": "2026-03-28T10:00:00Z",
"shards": [
{ "year": 2024, "url": "index_2024.json", "count": 312 }
],
"activities": [ ... ]
}
```
### Fields
| Field | Type | Required | Description |
|---|---|---|---|
| `bas_version` | string | yes | Schema version. Currently `"1.0"`. |
| `owner.handle` | string | yes | URL-safe identifier, e.g. `"brutsalvadi"`. |
| `owner.display_name` | string | yes | Human-readable name. |
| `owner.avatar_url` | string\|null | no | Absolute URL to an avatar image. |
| `generated_at` | string | yes | ISO 8601 timestamp of when this file was generated. |
| `shards` | array | no | Pointers to yearly shard files. See below. |
| `activities` | array | yes | Array of **Activity Summary** objects. May be empty. |
`index.json` should contain all activities when the total count is under ~5,000.
Above that, use yearly shards and keep only the most recent 200 activities
inline in `index.json` for fast feed rendering.
### Shard object
| Field | Type | Description |
|---|---|---|
| `year` | integer | Calendar year covered by this shard. |
| `url` | string | Relative or absolute URL to the shard file. |
| `count` | integer | Number of activities in the shard. |
---
## Activity Summary object
Appears in `index.json` (and yearly shard files). Contains only the fields
needed to render an activity card in a feed — no timeseries, no full track.
```json
{
"id": "2024-06-01T073012Z-morning-ride",
"title": "Morning Ride",
"sport": "cycling",
"sub_sport": "road",
"started_at": "2024-06-01T07:30:12+02:00",
"distance_m": 42300.0,
"duration_s": 5400,
"moving_time_s": 5100,
"elevation_gain_m": 620.0,
"avg_speed_kmh": 28.2,
"max_speed_kmh": 52.1,
"avg_hr_bpm": 148,
"max_hr_bpm": 178,
"avg_cadence_rpm": 88,
"avg_power_w": null,
"source": "strava_export",
"privacy": "public",
"detail_url": "activities/2024-06-01T073012Z-morning-ride.json",
"track_url": "activities/2024-06-01T073012Z-morning-ride.geojson"
}
```
### Fields
| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Unique identifier. See **Activity ID** section. |
| `title` | string | yes | Human-readable name. May be auto-generated if not in source. |
| `sport` | string | yes | One of: `cycling`, `running`, `hiking`, `walking`, `swimming`, `skiing`, `other`. |
| `sub_sport` | string\|null | no | e.g. `road`, `mountain`, `gravel`, `indoor`, `trail`, `track`, `nordic`, `alpine`, `open_water`, `pool`. |
| `started_at` | string | yes | ISO 8601 timestamp with timezone. |
| `distance_m` | number\|null | no | Total distance in metres. |
| `duration_s` | integer\|null | no | Total elapsed time in seconds. |
| `moving_time_s` | integer\|null | no | Time in motion (stopped periods excluded). |
| `elevation_gain_m` | number\|null | no | Cumulative positive elevation in metres. |
| `avg_speed_kmh` | number\|null | no | Average speed over moving time. |
| `max_speed_kmh` | number\|null | no | Maximum instantaneous speed. |
| `avg_hr_bpm` | integer\|null | no | Average heart rate. |
| `max_hr_bpm` | integer\|null | no | Maximum heart rate. |
| `avg_cadence_rpm` | integer\|null | no | Average cadence (rpm for cycling, spm for running). |
| `avg_power_w` | integer\|null | no | Average power in watts. |
| `source` | string\|null | no | Origin of data. See **Source values**. |
| `privacy` | string | yes | One of: `public`, `blur_start`, `no_gps`, `unlisted`. (`private` is a deprecated alias for `unlisted`.) |
| `mmp` | array\|null | no | Mean Maximal Power curve — `[[duration_s, avg_watts], ...]`. |
| `best_efforts` | array\|null | no | Best efforts by distance — `[[distance_km, time_s], ...]`. |
| `best_climb_m` | number\|null | no | Best single climb in metres (Kadane's algorithm). |
| `detail_url` | string\|null | no | Relative or absolute URL to the full activity JSON. |
| `track_url` | string\|null | no | Relative or absolute URL to the GeoJSON track. `null` if `privacy` is `no_gps`. |
| `preview_coords` | array\|null | no | Simplified track preview — `[[lon, lat], ...]` for card thumbnails. |
### Activity ID
The canonical ID format is:
```
{started_at_compact}[-{slug}]
```
Where `started_at_compact` is the start timestamp with special characters
removed: `2024-06-01T073012Z`, and `slug` is an optional URL-safe
lowercase title (spaces → hyphens, non-ASCII stripped).
Example: `2024-06-01T073012Z-morning-ride`
IDs must be unique within a data store. When a title is unavailable, the
timestamp alone is sufficient: `2024-06-01T073012Z`.
### Source values
| Value | Description |
|---|---|
| `strava_export` | Strava bulk data export |
| `garmin_connect` | Garmin Connect bulk export |
| `wahoo` | Wahoo ELEMNT / SYSTM export |
| `komoot` | Komoot GPX export |
| `gpx_file` | Generic GPX file |
| `fit_file` | Generic FIT file |
| `tcx_file` | Generic TCX file |
| `karoo` | Hammerhead Karoo device export |
| `manual` | Manually created |
### Privacy levels
| Level | GPS track published | Timeseries lat/lon | Shown in feed |
|---|---|---|---|
| `public` | Full track | Included | Yes — everyone |
| `blur_start` | First/last 200 m removed | Trimmed | Yes — everyone |
| `no_gps` | Not published | Not included | Yes — everyone |
| `unlisted` | Full track | Included | No — owner only (via direct URL) |
| `private` | *(deprecated alias for `unlisted`)* | Included | No — owner only |
**`unlisted`** activities are not shown in the public feed but are fully accessible
by direct URL — the GPS track, timeseries, and detail JSON are all served as normal
static files. This is "security by obscurity": knowing the URL is sufficient to
access the activity. If you need true data exclusion, use `no_gps` for GPS removal
while keeping stats public, or delete the activity entirely.
The legacy `private` value is accepted everywhere `unlisted` is valid.
---
## `activities/{id}.json`
Full activity record. Extends the Summary with timeseries and metadata.
```json
{
"bas_version": "1.0",
"id": "2024-06-01T073012Z-morning-ride",
"title": "Morning Ride",
"description": "Easy morning spin before work.",
"sport": "cycling",
"sub_sport": "road",
"started_at": "2024-06-01T07:30:12+02:00",
"distance_m": 42300.0,
"duration_s": 5400,
"moving_time_s": 5100,
"elevation_gain_m": 620.0,
"elevation_loss_m": 615.0,
"avg_speed_kmh": 28.2,
"max_speed_kmh": 52.1,
"avg_hr_bpm": 148,
"max_hr_bpm": 178,
"avg_cadence_rpm": 88,
"avg_power_w": null,
"max_power_w": null,
"gear": "Canyon Ultimate CF SL",
"device": "Hammerhead Karoo 2",
"bbox": [9.1234, 45.4321, 9.5678, 45.8765],
"start_latlng": [45.4321, 9.1234],
"end_latlng": [45.4321, 9.1235],
"laps": [],
"timeseries": {
"t": [0, 1, 2],
"lat": [45.4321, 45.4322, 45.4323],
"lon": [9.1234, 9.1235, 9.1236],
"elevation_m": [120.0, 120.5, 121.0],
"speed_kmh": [0.0, 15.2, 22.4],
"hr_bpm": [null, 142, 145],
"cadence_rpm": [null, 85, 88],
"power_w": [null, null, null],
"temperature_c": [null, null, null]
},
"source": "karoo",
"source_file": "13957.activity.abc123.fit",
"source_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"strava_id": null,
"privacy": "public",
"custom": {}
}
```
### Additional fields (beyond Summary)
| Field | Type | Required | Description |
|---|---|---|---|
| `description` | string\|null | no | Free-text description. |
| `elevation_loss_m` | number\|null | no | Cumulative negative elevation. |
| `max_power_w` | integer\|null | no | Maximum power in watts. |
| `gear` | string\|null | no | Equipment used (bike name, shoe model…). |
| `device` | string\|null | no | Recording device (e.g. `"Garmin Edge 530"`). |
| `bbox` | array\|null | no | `[min_lon, min_lat, max_lon, max_lat]`. Null if no GPS. |
| `start_latlng` | array\|null | no | `[lat, lon]` of activity start. |
| `end_latlng` | array\|null | no | `[lat, lon]` of activity end. |
| `laps` | array | yes | Array of **Lap** objects. Empty array if no laps. |
| `timeseries` | object | yes | Parallel arrays of sensor data. See below. |
| `source_file` | string\|null | no | Original filename (basename only, no path). |
| `source_hash` | string\|null | no | `sha256:{hex}` of the original raw file bytes. Used for deduplication. |
| `strava_id` | string\|null | no | Strava activity ID if origin is a Strava export. |
| `custom` | object | yes | Free dict for plugin-computed fields. Must be present, may be `{}`. |
### Timeseries object
Parallel arrays, all the same length. Index `i` corresponds to `t[i]` seconds
after the activity start.
| Key | Type | Unit | Description |
|---|---|---|---|
| `t` | int[] | seconds | Seconds since `started_at`. Always present. |
| `lat` | float[]\|null | degrees | Latitude. `null` if no GPS or privacy=`no_gps`. |
| `lon` | float[]\|null | degrees | Longitude. `null` if no GPS or privacy=`no_gps`. |
| `elevation_m` | float[] | metres | Elevation. Array of nulls if unavailable. |
| `speed_kmh` | float[] | km/h | Speed. Array of nulls if unavailable. |
| `hr_bpm` | int[] | bpm | Heart rate. Array of nulls if no HR sensor. |
| `cadence_rpm` | int[] | rpm/spm | Cadence. Array of nulls if unavailable. |
| `power_w` | int[] | watts | Power. Array of nulls if no power meter. |
| `temperature_c` | float[] | °C | Temperature. Array of nulls if unavailable. |
Timeseries are downsampled to at most 1 sample per second. The exact
downsampling strategy is implementation-defined; linear interpolation or
nearest-neighbour are both acceptable.
`lat` and `lon` arrays are either both present (both non-null arrays) or both
`null`. Treat `null` the same as an array of nulls.
### Lap object
```json
{
"index": 0,
"started_at": "2024-06-01T07:30:12+02:00",
"duration_s": 1800,
"distance_m": 21150.0,
"elevation_gain_m": 310.0,
"avg_speed_kmh": 28.2,
"avg_hr_bpm": 145,
"avg_power_w": null
}
```
---
## `activities/{id}.geojson`
Simplified GPS track for map rendering. Omitted entirely when
`privacy` is `no_gps` or `private`.
```json
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[9.1234, 45.4321, 120.0],
[9.1235, 45.4322, 120.5]
]
},
"properties": {
"id": "2024-06-01T073012Z-morning-ride",
"speeds": [0.0, 15.2],
"simplification": "rdp",
"rdp_epsilon": 0.0001,
"point_count_original": 7200,
"point_count_simplified": 843
}
}
```
Coordinates are `[longitude, latitude, elevation_metres]` per GeoJSON spec.
The `speeds` property is a parallel array to `coordinates` — one speed value
per point — used for gradient coloring on the map.
---
## Deduplication
Activities from different sources (e.g. a Strava export and a Karoo export)
may represent the same real-world ride. Producers should detect and handle
duplicates before writing the data store.
### Exact duplicate
Two files with the same `source_hash` are byte-for-byte identical. Only one
should be processed; the other is silently skipped.
### Near-duplicate (same ride, different source)
Two activities are considered near-duplicates if:
- `|started_at difference|` < 5 minutes, **and**
- `|distance_m difference| / max(distance_m)` < 5%
When a near-duplicate is detected:
1. One is kept as the **canonical** record (priority: FIT > GPX > TCX,
then prefer the source with more sensor channels).
2. The duplicate is written with `"duplicate_of": "{canonical_id}"` and
`"privacy": "private"` so it is excluded from feeds but remains auditable.
### Deduplication metadata in detail record
```json
{
"source_hash": "sha256:e3b0c...",
"duplicate_of": null
}
```
| Field | Type | Description |
|---|---|---|
| `source_hash` | string\|null | `sha256:{hex}` of original file bytes. |
| `duplicate_of` | string\|null | ID of the canonical activity, if this is a duplicate. |
---
## Instance manifest (`index.json` — multi-user mode)
In multi-user mode, the root `index.json` is a **shard manifest** rather than a user feed. It lists pointers to per-user BAS feeds. The browser fetches all shards concurrently and merges them.
```json
{
"bas_version": "1.0",
"instance": {
"name": "Our Rides",
"private": true
},
"generated_at": "2026-04-07T10:00:00Z",
"shards": [
{ "handle": "dave", "url": "dave/_merged/index.json" },
{ "handle": "alice", "url": "alice/_merged/index.json" },
{ "handle": "bob", "url": "https://bob.example.com/index.json" }
],
"activities": []
}
```
### Fields
| Field | Type | Description |
|---|---|---|
| `instance.name` | string | Human-readable instance name. |
| `instance.private` | boolean | If `true`, the site redirects unauthenticated visitors to `/login/`. |
| `shards` | array | Per-user shard entries. |
### Shard object (multi-user)
| Field | Type | Description |
|---|---|---|
| `handle` | string | User handle. Used for attribution (activities show `@handle`). |
| `url` | string | Relative or absolute URL to the user's `index.json`. |
The `url` field is relative to the location of the root manifest. Absolute URLs (starting with `http`) are fetched cross-origin — this is the federation mechanism.
Each user's `{handle}/index.json` is a valid standalone BAS feed. It can be used independently or included in another instance's shard manifest (federation).
---
## Versioning
The `bas_version` field allows consumers to handle schema evolution. Consumers
should:
- Reject files with a major version higher than they support.
- Accept and ignore unknown fields (forward compatibility).
- Treat missing optional fields as `null` (backward compatibility).
Current version: **1.0**
---
## Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-03-28 | Initial release. |
+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