improve docs
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
@@ -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)
|
||||
|
||||

|
||||
```
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user