Dashboard backend

HUB documentation · backend/README.md

← Documentation library · Download Markdown

Production dashboard / HUB API: https://bamboubot.duckdns.org
Set HUB_URL=https://bamboubot.duckdns.org. Local-development commands and loopback bind addresses below are development/operator reference, not the production API origin. Use the existing hosted HUB; you do not need to deploy a new one.

Introduction

From HubDashboard/:

python -m pip install -r backend/requirements.txt
export HUB_ADMIN_PASSWORD='a-long-unique-password'
export HUB_DATA_DIR='./data'
uvicorn backend.app:app --host 0.0.0.0 --port 8000

HUB_ADMIN_PASSWORD is required; the process fails closed without it. Supply it through a secret manager in production, not a committed file. Use HTTPS at the reverse proxy. Only trust forwarded headers from your own proxy. The public health check is GET /api/health. OpenAPI/docs are disabled.

Storage and secrets

SQLite, signing.key, and (unless supplied through HUB_ENCRYPTION_KEY) encryption.key persist in HUB_DATA_DIR (default ./data, relative to cwd). Directory permissions are 0700; database/key files are 0600. Mount a persistent, private volume and back up the database together with both keys. Keys are generated once, never regenerated on restart. HUB_ENCRYPTION_KEY must be a valid Fernet key; changing/loss of this key makes existing secrets unreadable. Generate one with python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'.

API/captcha keys and proxy passwords are Fernet-encrypted. Agent tokens are random, stored only as SHA-256 hashes, and returned only on creation/rotation. Admin bearer tokens are signed with a persistent signing key, expire after eight hours, and are distinct from site-scoped agent tokens. Rotating an agent token immediately invalidates the old token. Treat agent config responses as secrets. Do not log request bodies or Authorization headers. Error/ack text is displayed verbatim to admins: agents must never include credentials in those messages.

Login is limited to 10 attempts per client IP per minute per process. For public deployments, add proxy-level distributed rate limiting. One Uvicorn worker is recommended; SQLite transactions also serialize multiple processes, but the login limiter is process-local. Key creation is atomic across competing workers.

Behavior and limits

Built frontend/dist is served by the API with SPA fallback. Unknown /api routes and missing assets never return the SPA. Until frontend is built, only API endpoints are available.

Agent local enrollment

GET /api/agent/config includes revision and local_sync_initialized. Site-token-only POST /api/agent/local-sync accepts a required nonnegative integer expected_revision, optional partial config (ai_api_key, number_of_workers, ai_preset_id only), and optional proxies_content. It returns the full decrypted agent configuration. Treat the response as secret.

The first enrollment imports local values only for a pristine site; validation, encrypted storage, initialization, and revision increment are one transaction. Even empty enrollment initializes the site. Later calls never reimport files. Any administrator config PUT or proxy add/delete, and any proxy import inserting at least one row, permanently prevents automatic local import and increments the revision. Dry runs, rejected requests, and duplicate-only imports do neither. A stale revision returns 409 without mutation: fetch the latest configuration, then retry with only its expected_revision to finalize safely.

Startup automatically adds persistent revision/enrollment columns to existing databases. All legacy sites are conservatively marked admin-modified: historical deletions cannot be inferred from an empty table. Their hub values remain authoritative; intentional proxy changes can use the admin import endpoint.

Tests

cd backend
python -m pip install -r requirements-dev.txt
python -m pytest

Tests use isolated temporary SQLite databases and key files, never production data. hub.app.create_app accepts explicit test settings without importing the production entrypoint (which intentionally requires the environment password).

Receipt-only chat, proxy, and AI telemetry

These endpoints store reports only. They never call an AI provider, enforce provider rate limits, execute chat logic, or update worker tasks, heartbeat state, or the existing whole-site /statistics snapshot. Callers define what counts as a chat, conversion, message, or link. Send counts, never URLs, transcripts, prompts, personal information, actual API keys, or proxy credentials. Do not log request bodies or Authorization headers. Validation responses do not echo input.

Agent writes (site-scoped agent bearer token only)

POST /api/agent/chats — required fields:

{"event_id":"UUID", "worker_id":"UUID", "msgs_in":2, "msgs_out":3,
 "links":1, "converted":false, "duration":12.5}

POST /api/agent/ai-usage — all fields required:

{"event_id":"UUID", "key_id":"primary-slot", "calls":10,
 "error_429":2, "error_502":1, "latency_ms":120.5}

Both POSTs return HTTP 200: {event_id:string, duplicate:boolean, timestamp:UTC-string}. The receipt's timestamp is unchanged on retries. Uniqueness is enforced in SQLite on (site_id,event_id) across both endpoints. An identical normalized body returns duplicate:true; reusing the identifier with another body or endpoint returns 409. Optional omitted and explicit-null fields normalize identically. No mutation happens on duplicate receipt. Generate one UUID per event and keep the exact payload for retries; separate sites may use the same event UUID.

POST /api/agent/errors retains existing {worker_id?,level,message} and HTTP 201 {id} response; adds optional nullable proxy_id:UUID and code:'proxy_blocked'|'proxy_auth'|'proxy_timeout'|'proxy_recovered'|'other'. level remains required (info|warning|error); explicit recovery can use info. Proxy ownership checks match chat writes. Credential-bearing URL/userinfo and obvious Authorization/key patterns are rejected in message text; agents must still sanitize all messages because arbitrary free text cannot reliably be classified as secret. Errors remain non-idempotent; no event_id was added. Existing admin error objects additionally include nullable proxy_id and code.

Admin reads (admin bearer token only)

Unknown sites return 404. Agent tokens cannot read admin telemetry; admin tokens cannot submit agent telemetry. All aggregations are retained-history views, not lifetime totals. Aggregate integer sums may exceed JavaScript's safe integer range even though individual event counters cannot; clients should account for that when displaying very large totals.

Retention, bounded storage, and migration

SQLite startup migrations are additive, transactional, and repeatable, including on existing databases. Site deletion cascades through all new tables. All writes, dedup checks, and pruning run within BEGIN IMMEDIATE, including across processes.

Chat and AI history share a cap of 9000 payloads/site, 90000 globally; oldest payloads are pruned first. Existing error history is capped at 1000/site and now 10000 globally (oldest first). Thus detailed telemetry is bounded conservatively to 10000/site and 100000 globally. Chat/AI payloads and aggregates additionally expire seven days after receipt. Reads exclude expired history immediately; physical expired receipt cleanup occurs on the next accepted telemetry write. Errors retain their existing count-based rather than age-based retention.

Pruning a payload preserves its UUID, payload hash, and timestamp for seven days after first receipt, so retries cannot recreate pruned history within that window. Receipt-only rows are separately capped at 20000/site, 200000 globally. At receipt capacity, new IDs return 429 with Retry-After: 3600 rather than evict unexpired deduplication data; existing IDs still retry normally. Stop retrying before seven days: after expiry, the same UUID is a new event and can count again. These are bounded storage admission limits, not AI/provider rate limits.

Deleting a proxy records only {proxy_id,site_id,timestamp} ownership, never credentials. Late reports can reference that tombstone for seven days, subject to conservative 9000/site and 90000 global oldest-first tombstone caps. Unknown, expired, or evicted historical references are rejected, not assigned to a site. Already-received chat retries still deduplicate even if the tombstone has gone. Deleted proxies from before this migration have no provable ownership and cannot be referenced by new reports. No historic totals or backfill are fabricated.

Telemetry capacity configuration

Set these environment variables before starting the backend; restart all backend processes together after changes. They apply to chat and AI events combined, not separately per endpoint:

Variable Default Meaning
HUB_TELEMETRY_HISTORY_SITE 9000 Retained chat/AI payloads per site
HUB_TELEMETRY_HISTORY_GLOBAL 90000 Retained chat/AI payloads globally
HUB_TELEMETRY_RECEIPTS_SITE 20000 Deduplication receipts per site
HUB_TELEMETRY_RECEIPTS_GLOBAL 200000 Deduplication receipts globally
HUB_TELEMETRY_RETENTION_DAYS 7 Payload/receipt lifetime assigned on receipt

All values must be positive decimal integers; invalid values fail startup without echoing their contents. Count settings cannot exceed 9223372036854775807; days cannot exceed 36500. These are validation ceilings, not recommended capacities. Keep site caps no higher than global caps and receipt caps at least as high as payload caps. Smaller receipt caps are permitted but become the effective intake limit. Raising limits increases SQLite disk use, pruning work, and aggregate response cost; size them for the deployment. Error history caps remain fixed. Deleted-proxy reference caps follow the configured payload caps and their lifetime is assigned using the configured retention days at deletion.

Batch AI usage per key per minute is recommended, rather than sending one event per HTTP attempt. For example, 20 calls/minute grouped into one batch per minute use 10080 receipts/key over seven days rather than 201600; include chat events and all keys/workers when budgeting capacity. Sum attempts/errors and compute the call-weighted batch latency locally. Assign one UUID to each batch, retain that exact batch for retries, and do not send overlapping batches. This is reporting guidance only: the hub does not schedule batches or limit AI calls.

Budget receipts as (chat events + AI batches) per day * retention days, with headroom for peaks, separately per site and globally. The conservative defaults can fill under normal high-volume traffic; override them for the actual workload without changing code. New IDs receive 429 at capacity; unexpired dedup receipts are never evicted to make room. Reducing a cap below current usage likewise rejects new IDs until usage falls; retries of existing receipts continue working.

Each receipt now persists its expiration timestamp. Changing retention days affects new receipts only, never shortens or extends existing promises; pre-configuration receipts migrate with their original seven-day expiration. Likewise, existing deleted-proxy references retain their assigned expiration (but remain subject to their separately documented oldest-first count caps). This supersedes the fixed seven-day wording above when an override is supplied.