reorg documentation
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# Architecture
|
||||
|
||||
BincioActivity is a two-stage pipeline that produces a self-contained static website from raw activity files.
|
||||
|
||||
```
|
||||
GPX / FIT / TCX files
|
||||
│
|
||||
▼
|
||||
bincio extract (Python)
|
||||
│
|
||||
▼
|
||||
BAS data store (plain JSON + GeoJSON files)
|
||||
│
|
||||
▼
|
||||
bincio render (wraps Astro build)
|
||||
│
|
||||
▼
|
||||
site/dist/ (static HTML/JS/CSS)
|
||||
│
|
||||
▼
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Stages
|
||||
|
||||
### Stage 1 — Extract (`bincio/extract/`)
|
||||
|
||||
Reads raw activity files, computes stats, and writes BAS JSON.
|
||||
|
||||
Key modules:
|
||||
|
||||
| Module | Role |
|
||||
|---|---|
|
||||
| `parsers/` | GPX, FIT, TCX parsers + format detection |
|
||||
| `metrics.py` | Haversine-based stats computation (single pass) |
|
||||
| `timeseries.py` | Downsample to 1 Hz, build BAS timeseries object |
|
||||
| `simplify.py` | RDP track simplification → GeoJSON |
|
||||
| `dedup.py` | Exact (hash) + near-duplicate detection |
|
||||
| `strava_csv.py` | Strava activities.csv metadata enrichment |
|
||||
| `writer.py` | BAS JSON + GeoJSON writer |
|
||||
| `config.py` | `extract_config.yaml` loader |
|
||||
|
||||
Extract is incremental: unchanged files (same SHA-256) are skipped. To force a full re-extract, delete the output directory.
|
||||
|
||||
Large data is passed to worker processes once per worker via `initializer=` (not once per task), keeping ProcessPoolExecutor overhead low.
|
||||
|
||||
### Stage 2 — Render (`bincio/render/`)
|
||||
|
||||
Merges sidecar edits, symlinks data, and runs `astro build`.
|
||||
|
||||
```
|
||||
data_dir/
|
||||
activities/ ← immutable extract output
|
||||
edits/ ← user-written sidecar markdown files
|
||||
_merged/ ← render-time merge output (served to browser)
|
||||
```
|
||||
|
||||
`merge_all()` overlays sidecar fields onto extracted JSON and writes `_merged/`. The browser always reads from `_merged/`.
|
||||
|
||||
---
|
||||
|
||||
## Site (`site/`)
|
||||
|
||||
Astro + Svelte + Tailwind + MapLibre GL + Observable Plot.
|
||||
|
||||
All data fetching is client-side — the site is fully static. On page load, the browser fetches `index.json`, resolves shards, and renders the feed.
|
||||
|
||||
Key components:
|
||||
|
||||
| Component | Role |
|
||||
|---|---|
|
||||
| `ActivityFeed.svelte` | Card grid, sport filter, pagination |
|
||||
| `ActivityDetail.svelte` | Map + stats + charts + photo gallery |
|
||||
| `ActivityMap.svelte` | MapLibre GL (gradient track, hover marker) |
|
||||
| `ActivityCharts.svelte` | Observable Plot (elevation/speed/HR/cadence) |
|
||||
| `StatsView.svelte` | Yearly heatmap + totals |
|
||||
| `EditDrawer.svelte` | Slide-in edit panel (visible when edit server is running) |
|
||||
|
||||
### Data loading
|
||||
|
||||
`site/src/lib/dataloader.ts` fetches `index.json` and recursively resolves shard URLs. Shards are fetched concurrently. The same mechanism handles yearly pagination and multi-user federation.
|
||||
|
||||
```
|
||||
index.json
|
||||
└── shards: [
|
||||
{ url: "dave/_merged/index.json" }, ← user shard
|
||||
{ url: "https://alice.example.com/index.json" } ← federated instance
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment modes
|
||||
|
||||
### Single-user (static)
|
||||
|
||||
No server process required. Run `bincio render`, drop `site/dist/` anywhere. The edit drawer requires `bincio edit` running locally and `PUBLIC_EDIT_URL` set in `site/.env`.
|
||||
|
||||
### Multi-user (VPS)
|
||||
|
||||
```
|
||||
internet
|
||||
│
|
||||
▼
|
||||
nginx / caddy
|
||||
├── /* → static files (site/dist/)
|
||||
└── /api/* → proxy → bincio serve (127.0.0.1:4041)
|
||||
```
|
||||
|
||||
`bincio serve` is a FastAPI application that owns auth, user management, and write operations. It never serves static files. nginx handles TLS and static file serving.
|
||||
|
||||
Data is partitioned per user:
|
||||
|
||||
```
|
||||
/data/
|
||||
instance.db ← SQLite: users, sessions, invites
|
||||
index.json ← root shard manifest (no activity data)
|
||||
{handle}/
|
||||
index.json ← user's BAS feed
|
||||
_merged/ ← sidecar-merged output
|
||||
activities/
|
||||
edits/
|
||||
```
|
||||
|
||||
The root `index.json` is a shard manifest that lists user shard URLs. The browser resolves all shards concurrently and merges them into a single feed.
|
||||
|
||||
### Instance privacy
|
||||
|
||||
When `instance.private = true` in the root `index.json`, the site's `Base.astro` layout injects a client-side auth wall: it calls `GET /api/me` on every page load and redirects to `/login/` on 401/404. The `/login/` and `/register/` pages opt out of this wall via `public={true}`.
|
||||
|
||||
This is a best-effort client-side guard. The static files themselves are always readable by anyone with direct URL access. Use nginx-level auth if you need true access control on the static assets.
|
||||
|
||||
---
|
||||
|
||||
## Edit flow
|
||||
|
||||
```
|
||||
Browser (EditDrawer.svelte)
|
||||
│ POST /api/activity/{id}
|
||||
▼
|
||||
bincio edit / bincio serve
|
||||
│ writes edits/{id}.md
|
||||
│ calls merge_all()
|
||||
▼
|
||||
_merged/{id}.json updated
|
||||
```
|
||||
|
||||
In multi-user mode, `bincio serve` additionally spawns `bincio render --handle {user}` to rewrite the shard manifest after each save.
|
||||
|
||||
---
|
||||
|
||||
## Federation
|
||||
|
||||
Any BAS-compliant feed can be included in the root `index.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"shards": [
|
||||
{ "handle": "dave", "url": "dave/_merged/index.json" },
|
||||
{ "handle": "alice", "url": "https://alice.example.com/index.json" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Remote activities appear in the combined feed with `@alice` attribution. The browser fetches remote shards directly — there is no server-side aggregation.
|
||||
|
||||
---
|
||||
|
||||
## Key design decisions
|
||||
|
||||
- **No database, no server** — everything is static files except in multi-user VPS mode, where `bincio serve` owns only the auth and write API.
|
||||
- **Haversine (not geopy)** for distance calculations — 10× faster for bulk processing.
|
||||
- **Iterative RDP** for track simplification — no `rdp` PyPI package dependency (not available as a pure-Python wheel for Pyodide).
|
||||
- **Worker initializer pattern** — large shared dicts (Strava lookup, known hashes) are sent once per worker process, not once per task.
|
||||
- **BAS activity IDs always use UTC with Z suffix** — URL-safe, unambiguous, sortable.
|
||||
- **TCX files** from Garmin use both `http://` and `https://` namespace URIs — the parser handles both.
|
||||
- **Shard manifest for multi-user** — no activity data duplication; each user's feed is a valid standalone BAS feed; the root manifest just points at them.
|
||||
@@ -0,0 +1,213 @@
|
||||
# Multi-user deployment
|
||||
|
||||
Multiple users share one bincio instance. Activities are public within the instance by default. The `private` flag hides individual activities. The whole instance requires login to view (private by default).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
internet
|
||||
│
|
||||
▼
|
||||
nginx / caddy
|
||||
├── /* → static files (site/dist/)
|
||||
└── /api/* → proxy → bincio serve (127.0.0.1:4041)
|
||||
```
|
||||
|
||||
`bincio serve` owns all dynamic behaviour — auth, user management, write operations. nginx serves static files and proxies API routes. `bincio serve` never handles static files.
|
||||
|
||||
Sessions are httpOnly cookies (`bincio_session`), stored in SQLite. The Astro site calls `GET /api/me` on page load to detect the logged-in user.
|
||||
|
||||
## Data layout
|
||||
|
||||
```
|
||||
/data/ ← BINCIO_DATA_DIR
|
||||
instance.db ← SQLite: users, sessions, invites
|
||||
index.json ← shard manifest (no activity data)
|
||||
{handle}/
|
||||
index.json ← user's BAS feed (activities)
|
||||
_merged/ ← sidecar-merged output (served to browser)
|
||||
activities/
|
||||
edits/
|
||||
strava_token.json
|
||||
```
|
||||
|
||||
The root `index.json` is a shard manifest — it lists user shard URLs but contains no activity data. Each user's `{handle}/index.json` is a valid standalone BAS feed (usable for federation). The browser resolves shards concurrently and merges them.
|
||||
|
||||
## Step 1 — Initialise the instance
|
||||
|
||||
```bash
|
||||
uv sync --extra serve
|
||||
|
||||
uv run bincio init \
|
||||
--data-dir /var/bincio \
|
||||
--handle dave \
|
||||
--password 'your-password' \
|
||||
--display-name "Dave" \
|
||||
--name "My Bincio"
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `/var/bincio/instance.db` — SQLite database
|
||||
- `/var/bincio/dave/` — admin user data directory
|
||||
- `/var/bincio/index.json` — root shard manifest (with `"private": true`)
|
||||
- Prints a first invite code
|
||||
|
||||
`bincio init` is idempotent — safe to re-run.
|
||||
|
||||
## Step 2 — Extract activities
|
||||
|
||||
```bash
|
||||
uv run bincio extract \
|
||||
--input ~/activity-files \
|
||||
--output /var/bincio/dave
|
||||
```
|
||||
|
||||
## Step 3 — Build the site
|
||||
|
||||
```bash
|
||||
cd site && npm install && cd ..
|
||||
|
||||
uv run bincio render \
|
||||
--data-dir /var/bincio \
|
||||
--site-dir site
|
||||
|
||||
# Output: site/dist/
|
||||
```
|
||||
|
||||
In multi-user mode, `bincio render`:
|
||||
- Runs `merge_all()` for each user's directory
|
||||
- Rewrites the root `index.json` shard manifest
|
||||
- Symlinks `site/public/data → /var/bincio`
|
||||
- Builds the Astro site
|
||||
|
||||
Incremental rebuild (one user only):
|
||||
|
||||
```bash
|
||||
uv run bincio render --data-dir /var/bincio --handle dave
|
||||
# Re-merges dave's shard, rewrites root manifest — does not rebuild the site
|
||||
```
|
||||
|
||||
## Step 4 — Configure nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name example.com;
|
||||
|
||||
root /var/www/bincio; # → site/dist/
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:4041;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Copy `site/dist/` to `/var/www/bincio` after each build.
|
||||
|
||||
## Step 5 — Start bincio serve
|
||||
|
||||
```bash
|
||||
uv run bincio serve \
|
||||
--data-dir /var/bincio \
|
||||
--site-dir /path/to/site
|
||||
```
|
||||
|
||||
As a systemd service:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=bincio serve
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=bincio
|
||||
WorkingDirectory=/home/bincio/bincio-activity
|
||||
ExecStart=uv run bincio serve --data-dir /var/bincio --site-dir site
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Inviting users
|
||||
|
||||
After initialising, `bincio init` prints an invite code. To generate more:
|
||||
|
||||
```bash
|
||||
# From the admin account, via the browser at /invites/
|
||||
# Or directly in the database:
|
||||
python3 -c "
|
||||
from pathlib import Path
|
||||
from bincio.serve.db import open_db, create_invite
|
||||
db = open_db(Path('/var/bincio'))
|
||||
print(create_invite(db, 'dave'))
|
||||
"
|
||||
```
|
||||
|
||||
Share the invite link: `https://example.com/register/?code=XXXXXXXX`
|
||||
|
||||
Invite limits: admins — unlimited. Regular users — 3 invites each (configurable in `bincio/serve/db.py`, `_MAX_USER_INVITES`).
|
||||
|
||||
## Instance privacy
|
||||
|
||||
By default, `bincio init` sets `"private": true` in the root `index.json`. This means every page (except `/login/` and `/register/`) redirects unauthenticated visitors to `/login/`.
|
||||
|
||||
To make the instance public, edit `/var/bincio/index.json` and set `"private": false`. The next `bincio render` will preserve this setting.
|
||||
|
||||
## Local testing (before deploying)
|
||||
|
||||
```bash
|
||||
# 1. Initialise a test instance
|
||||
uv run bincio init --data-dir /tmp/bincio_test --handle dave --password test
|
||||
|
||||
# 2. Extract activities into the user's dir
|
||||
uv run bincio extract --input ~/activity-files --output /tmp/bincio_test/dave
|
||||
|
||||
# 3. Build + start the dev server (terminal 1)
|
||||
uv run bincio render --data-dir /tmp/bincio_test --site-dir site --serve
|
||||
|
||||
# 4. Start bincio serve (terminal 2)
|
||||
uv run bincio serve --data-dir /tmp/bincio_test
|
||||
```
|
||||
|
||||
The Astro dev server proxies `/api/*` to `localhost:4041` (configured in `astro.config.mjs`), so cookies work same-origin. Set `site/.env`:
|
||||
|
||||
```
|
||||
BINCIO_DATA_DIR=/tmp/bincio_test
|
||||
PUBLIC_EDIT_URL=
|
||||
```
|
||||
|
||||
`PUBLIC_EDIT_URL` empty = edit UI enabled via proxy. The edit/upload button appears when `bincio serve` is running. In production nginx plays the same proxy role.
|
||||
|
||||
## Per-user Strava sync
|
||||
|
||||
Each user connects their own Strava account. The OAuth token is stored in `/var/bincio/{handle}/strava_token.json`. The "Connect Strava" and "Sync" buttons in the upload modal work per-session — each user syncs only their own activities.
|
||||
|
||||
## Federation
|
||||
|
||||
To follow another bincio instance, add a shard entry to the root `index.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"shards": [
|
||||
{ "handle": "dave", "url": "dave/_merged/index.json" },
|
||||
{ "handle": "alice", "url": "https://alice.example.com/index.json" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The browser fetches and merges remote shards concurrently. Remote activities appear in the combined feed with `@alice` attribution.
|
||||
|
||||
## See also
|
||||
|
||||
- [CLI reference — bincio init](../reference/cli.md#bincio-init)
|
||||
- [CLI reference — bincio serve](../reference/cli.md#bincio-serve)
|
||||
- [API reference](../reference/api.md)
|
||||
- [BAS schema — instance manifest](../../SCHEMA.md#instance-manifest)
|
||||
@@ -0,0 +1,86 @@
|
||||
# Single-user deployment
|
||||
|
||||
One person, one machine, all your data stays with you. This is the default and simplest mode.
|
||||
|
||||
## GitHub Pages (free, automated)
|
||||
|
||||
```bash
|
||||
uv run bincio render --deploy github
|
||||
```
|
||||
|
||||
This builds `site/dist/` and pushes it to the `gh-pages` branch. Requires `npx gh-pages` (`npm install -g gh-pages`).
|
||||
|
||||
Set the repository to serve from the `gh-pages` branch in GitHub → Settings → Pages.
|
||||
|
||||
## Static hosting (Netlify, Vercel, Cloudflare Pages, etc.)
|
||||
|
||||
Build locally and deploy the `site/dist/` directory. Or set up CI:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml (example)
|
||||
- run: uv run bincio render
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: site/dist
|
||||
```
|
||||
|
||||
## VPS with nginx
|
||||
|
||||
Serve `site/dist/` as a static directory. No server process needed for read-only access.
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /var/www/bincio/dist;
|
||||
index index.html;
|
||||
location / { try_files $uri $uri/ $uri.html =404; }
|
||||
}
|
||||
```
|
||||
|
||||
### Enable the edit UI on a VPS
|
||||
|
||||
If you want to edit activities from the browser while on your VPS:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name example.com;
|
||||
|
||||
root /var/www/bincio/dist;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
# Proxy /api/* to bincio edit (local-only, never exposed directly)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:4041;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run `bincio edit` as a background service:
|
||||
|
||||
```bash
|
||||
uv run bincio edit --data-dir ~/bincio_data
|
||||
```
|
||||
|
||||
And set `PUBLIC_EDIT_URL=` (empty — the proxy makes /api/ same-origin) in your environment before building.
|
||||
|
||||
## Keeping the site up to date
|
||||
|
||||
After extracting new activities or editing sidecars:
|
||||
|
||||
```bash
|
||||
uv run bincio extract # process new files
|
||||
uv run bincio render # rebuild site/dist/
|
||||
rsync -av site/dist/ user@server:/var/www/bincio/dist/
|
||||
```
|
||||
|
||||
Or automate with a cron job or GitHub Action.
|
||||
|
||||
## Privacy note
|
||||
|
||||
Single-user mode has no authentication. The site is public to anyone with the URL. Use `privacy: private` in sidecar files to hide specific activities, or restrict access at the nginx level (HTTP basic auth, IP allowlist, etc.).
|
||||
@@ -0,0 +1,88 @@
|
||||
# Getting started
|
||||
|
||||
BincioActivity turns a folder of GPX/FIT/TCX files into a static website you host yourself. No database. No cloud dependency. No account.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python ≥ 3.12 and [uv](https://docs.astral.sh/uv/)
|
||||
- Node ≥ 20 and npm (for the site)
|
||||
- Your activity files (Strava export, Garmin export, Karoo, etc.)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/brutsalvadi/bincio-activity.git
|
||||
cd bincio-activity
|
||||
uv sync
|
||||
```
|
||||
|
||||
## Configure
|
||||
|
||||
```bash
|
||||
cp extract_config.example.yaml extract_config.yaml
|
||||
$EDITOR extract_config.yaml
|
||||
```
|
||||
|
||||
Minimum configuration:
|
||||
|
||||
```yaml
|
||||
owner:
|
||||
handle: yourname # used in URLs and federation
|
||||
display_name: Your Name
|
||||
|
||||
input:
|
||||
dirs:
|
||||
- ~/your-activity-data/activities
|
||||
|
||||
output:
|
||||
dir: ~/bincio_data
|
||||
```
|
||||
|
||||
The config file is gitignored — safe to store Strava credentials here.
|
||||
|
||||
## Extract
|
||||
|
||||
```bash
|
||||
uv run bincio extract
|
||||
```
|
||||
|
||||
This reads all GPX/FIT/TCX files (including `.gz` variants), deduplicates them, and writes a BAS data store to `~/bincio_data/`.
|
||||
|
||||
Re-running is safe — unchanged files are skipped (hash-based). To force a full re-extract: `rm -rf ~/bincio_data && uv run bincio extract`.
|
||||
|
||||
## Build the site
|
||||
|
||||
```bash
|
||||
cd site && npm install && cd ..
|
||||
cp site/.env.example site/.env
|
||||
# Edit site/.env: set BINCIO_DATA_DIR=~/bincio_data
|
||||
uv run bincio render
|
||||
```
|
||||
|
||||
Output is in `site/dist/` — a folder of static files. Drop it anywhere: GitHub Pages, Netlify, a Raspberry Pi, a USB stick.
|
||||
|
||||
## Dev mode
|
||||
|
||||
```bash
|
||||
uv run bincio render --serve # → http://localhost:4321
|
||||
```
|
||||
|
||||
## Enable the edit UI
|
||||
|
||||
The edit UI lets you rename activities, add descriptions, upload photos, and sync from Strava — all from the browser.
|
||||
|
||||
```bash
|
||||
uv sync --extra edit
|
||||
uv run bincio edit # starts on http://localhost:4041
|
||||
# Add to site/.env:
|
||||
# PUBLIC_EDIT_URL=http://localhost:4041
|
||||
```
|
||||
|
||||
An Edit button and an Upload ↑ button appear in the nav.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Single-user deployment](deployment/single-user.md) — serve your site on a VPS or GitHub Pages
|
||||
- [Multi-user deployment](deployment/multi-user.md) — invite friends, shared feed
|
||||
- [CLI reference](reference/cli.md) — all commands and options
|
||||
- [BAS schema](../SCHEMA.md) — the data format and federation protocol
|
||||
@@ -0,0 +1,208 @@
|
||||
# API reference
|
||||
|
||||
`bincio serve` exposes a JSON API on `/api/*`. In production, nginx proxies these routes from the public domain. In local development, Vite proxies them from `astro dev`.
|
||||
|
||||
All request and response bodies are `application/json`. Authentication uses an httpOnly session cookie (`bincio_session`).
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### `GET /api/me`
|
||||
|
||||
Returns the currently authenticated user, or 404 if not logged in.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
{
|
||||
"handle": "dave",
|
||||
"display_name": "Dave",
|
||||
"is_admin": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404** — not authenticated
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/login`
|
||||
|
||||
Rate-limited: 10 attempts per 15 minutes per IP.
|
||||
|
||||
**Request**
|
||||
```json
|
||||
{ "handle": "dave", "password": "your-password" }
|
||||
```
|
||||
|
||||
**Response 200** — sets `bincio_session` cookie (httpOnly, SameSite=Lax, 30-day max-age)
|
||||
```json
|
||||
{ "ok": true, "handle": "dave", "display_name": "Dave" }
|
||||
```
|
||||
|
||||
**Response 401** — invalid credentials
|
||||
**Response 429** — rate limit exceeded
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/logout`
|
||||
|
||||
Deletes the session from the database and clears the cookie.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Registration
|
||||
|
||||
### `POST /api/register`
|
||||
|
||||
Creates a new user account using a valid invite code.
|
||||
|
||||
**Request**
|
||||
```json
|
||||
{
|
||||
"code": "ABCD1234",
|
||||
"handle": "alice",
|
||||
"password": "my-password",
|
||||
"display_name": "Alice"
|
||||
}
|
||||
```
|
||||
|
||||
Handle rules: lowercase letters, numbers, `_`, `-`; 1–30 characters.
|
||||
Password: minimum 8 characters.
|
||||
|
||||
**Response 200** — sets session cookie, logs in immediately
|
||||
```json
|
||||
{ "ok": true, "handle": "alice" }
|
||||
```
|
||||
|
||||
**Response 400** — invalid handle, password too short, or invalid/used invite code
|
||||
**Response 409** — handle already taken
|
||||
|
||||
---
|
||||
|
||||
## Invites
|
||||
|
||||
All invite endpoints require authentication.
|
||||
|
||||
### `GET /api/invites`
|
||||
|
||||
Lists invite codes created by the current user.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"code": "ABCD1234",
|
||||
"used": false,
|
||||
"used_by": null,
|
||||
"created_at": "2026-04-01T10:00:00Z",
|
||||
"used_at": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/invites`
|
||||
|
||||
Generates a new invite code for the current user. Regular users are limited to 3 invites; admins are unlimited.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
{ "ok": true, "code": "EFGH5678" }
|
||||
```
|
||||
|
||||
**Response 400** — invite limit reached
|
||||
|
||||
---
|
||||
|
||||
## Admin
|
||||
|
||||
### `GET /api/admin/users`
|
||||
|
||||
Lists all users. Admin only.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"handle": "dave",
|
||||
"display_name": "Dave",
|
||||
"is_admin": true,
|
||||
"created_at": "2026-03-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Response 403** — not an admin
|
||||
|
||||
---
|
||||
|
||||
## Write API
|
||||
|
||||
All write endpoints require authentication. Users can only read/write their own activities.
|
||||
|
||||
### `GET /api/activity/{activity_id}`
|
||||
|
||||
Returns the full activity JSON for an activity owned by the current user.
|
||||
|
||||
**Response 200** — BAS activity detail object
|
||||
**Response 404** — activity not found or not owned by user
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/activity/{activity_id}`
|
||||
|
||||
Writes a sidecar edit for an activity. Triggers an incremental shard rebuild if `--site-dir` was passed to `bincio serve`.
|
||||
|
||||
**Request**
|
||||
```json
|
||||
{
|
||||
"title": "Epic climb",
|
||||
"description": "Rode with friends.",
|
||||
"sport": "cycling",
|
||||
"private": false,
|
||||
"highlight": false,
|
||||
"gear": "Trek Domane"
|
||||
}
|
||||
```
|
||||
|
||||
All fields are optional. Only provided fields are written to the sidecar.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/strava/sync`
|
||||
|
||||
Triggers a Strava sync for the current user's data directory. Uses the stored OAuth token in `{handle}/strava_token.json`.
|
||||
|
||||
**Response 200**
|
||||
```json
|
||||
{ "new_count": 3, "error_count": 0 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error format
|
||||
|
||||
All errors follow FastAPI's default format:
|
||||
|
||||
```json
|
||||
{ "detail": "Invalid credentials" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The session cookie is `SameSite=Lax`. The server sets `secure=False` because TLS termination is handled by nginx/caddy. If you serve `bincio serve` directly on HTTPS (not recommended), set `secure=True` in `server.py`.
|
||||
- There is no CSRF protection — the API relies on the same-origin constraint enforced by `SameSite=Lax` cookies.
|
||||
- The CORS policy allows `localhost:*` origins for local development only. Cross-origin requests from production domains are blocked — all traffic must go through the nginx proxy.
|
||||
@@ -0,0 +1,160 @@
|
||||
# CLI reference
|
||||
|
||||
All commands are run via `uv run bincio <command>` from the project root.
|
||||
|
||||
---
|
||||
|
||||
## bincio extract
|
||||
|
||||
Extract GPX/FIT/TCX files into a BAS data store.
|
||||
|
||||
```bash
|
||||
uv run bincio extract [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--config PATH` | `extract_config.yaml` | Path to config file |
|
||||
| `--input DIR` | from config | Input directory (scanned recursively) |
|
||||
| `--output DIR` | from config | Output BAS data store directory |
|
||||
| `--file PATH` | — | Extract a single file, print JSON to stdout |
|
||||
| `--since DATE` | — | Only process files newer than this date (YYYY-MM-DD) |
|
||||
| `--dev N` | — | Dev mode: sample N files evenly, output to `/tmp/bincio_dev/` |
|
||||
|
||||
Extraction is incremental by default — unchanged files (same hash) are skipped. To force a full re-extract: `rm -rf <output_dir>`.
|
||||
|
||||
Supported formats: GPX, FIT, TCX — all with optional `.gz` compression.
|
||||
|
||||
---
|
||||
|
||||
## bincio render
|
||||
|
||||
Merge sidecar edits and build (or serve) the Astro site.
|
||||
|
||||
```bash
|
||||
uv run bincio render [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--data-dir DIR` | auto-detected | BAS data store |
|
||||
| `--site-dir DIR` | `./site` | Astro project directory |
|
||||
| `--out DIR` | `site/dist` | Build output directory |
|
||||
| `--serve` | false | Start dev server instead of building |
|
||||
| `--deploy TARGET` | — | Deploy after build. Currently: `github` |
|
||||
| `--handle HANDLE` | — | (Multi-user) Re-merge one user's shard only, then rewrite root manifest |
|
||||
|
||||
`bincio render` always:
|
||||
1. Runs `merge_all()` — applies sidecar edits, produces `_merged/`
|
||||
2. (Multi-user) Rewrites the root `index.json` shard manifest
|
||||
3. Symlinks `site/public/data` → data directory
|
||||
4. Runs `astro build` (or `astro dev` with `--serve`)
|
||||
|
||||
Data directory auto-detection order:
|
||||
1. `--data-dir` flag
|
||||
2. `output.dir` in `extract_config.yaml` (if found in cwd)
|
||||
3. `./site/public/data` (symlink)
|
||||
4. `../bincio_data`
|
||||
|
||||
---
|
||||
|
||||
## bincio edit
|
||||
|
||||
Start the local single-user edit server. For personal use only — no authentication.
|
||||
|
||||
```bash
|
||||
uv sync --extra edit # install dependencies (one-time)
|
||||
uv run bincio edit [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--data-dir DIR` | auto-detected | BAS data store |
|
||||
| `--host HOST` | `127.0.0.1` | Bind address |
|
||||
| `--port PORT` | `4041` | Bind port |
|
||||
| `--strava-client-id ID` | from config | Strava OAuth client ID |
|
||||
| `--strava-client-secret SECRET` | from config | Strava OAuth client secret |
|
||||
|
||||
Set `PUBLIC_EDIT_URL=http://localhost:4041` in `site/.env` to enable the Edit button and Upload ↑ button in the site.
|
||||
|
||||
Credentials resolution: `--strava-client-*` flags → `STRAVA_CLIENT_ID/SECRET` env vars → `import.strava.*` in `extract_config.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## bincio init
|
||||
|
||||
Bootstrap a fresh multi-user instance. Run once per VPS.
|
||||
|
||||
```bash
|
||||
uv sync --extra serve # install dependencies (one-time)
|
||||
uv run bincio init [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Required | Description |
|
||||
|---|---|---|
|
||||
| `--data-dir DIR` | yes | BAS data directory to initialise |
|
||||
| `--handle HANDLE` | yes | Admin user handle (lowercase, URL-safe) |
|
||||
| `--password PASSWORD` | yes | Admin password (prompted if omitted) |
|
||||
| `--display-name NAME` | no | Admin display name (defaults to handle) |
|
||||
| `--name NAME` | no | Instance name shown in the feed |
|
||||
|
||||
Creates:
|
||||
- `instance.db` — SQLite database with users/sessions/invites tables
|
||||
- `{handle}/` — admin user data directory and subdirectories
|
||||
- `index.json` — root shard manifest with `"private": true`
|
||||
- Prints a first invite code to stdout
|
||||
|
||||
Idempotent — safe to re-run. Skips steps already completed.
|
||||
|
||||
---
|
||||
|
||||
## bincio serve
|
||||
|
||||
Start the multi-user application server (VPS mode).
|
||||
|
||||
```bash
|
||||
uv run bincio serve [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--data-dir DIR` | required | BAS data directory (must contain `instance.db`) |
|
||||
| `--site-dir DIR` | — | Astro site dir — enables post-write incremental rebuilds |
|
||||
| `--host HOST` | `127.0.0.1` | Bind address (keep on localhost; nginx proxies from outside) |
|
||||
| `--port PORT` | `4041` | Bind port |
|
||||
|
||||
Requires `bincio init` to have been run first. Handles auth, user management, and write operations. nginx is responsible for serving static files and proxying `/api/*` to this server.
|
||||
|
||||
See [multi-user deployment](../deployment/multi-user.md) for nginx configuration.
|
||||
|
||||
---
|
||||
|
||||
## bincio import strava
|
||||
|
||||
Import activities directly from the Strava API.
|
||||
|
||||
```bash
|
||||
uv sync --extra strava
|
||||
uv run bincio import strava [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `--output DIR` | from config | BAS data store output directory |
|
||||
| `--since DATE` | last sync | Only import activities after this date |
|
||||
| `--reauth` | false | Force a new OAuth flow even if a token exists |
|
||||
| `--dev N` | — | Dev mode: import N most recent activities to `/tmp/bincio_dev/` |
|
||||
|
||||
Credentials: set `import.strava.client_id` and `import.strava.client_secret` in `extract_config.yaml`. The Authorization Callback Domain in the Strava app settings must be `localhost`.
|
||||
|
||||
Tokens are stored in `<data_dir>/strava_token.json` and auto-refreshed.
|
||||
|
||||
---
|
||||
|
||||
## Global flags
|
||||
|
||||
```bash
|
||||
uv run bincio --version # print version
|
||||
uv run bincio --help # list commands
|
||||
uv run bincio <cmd> --help # command-specific help
|
||||
```
|
||||
Reference in New Issue
Block a user