Files
PilotVault/API Server
tajniak81andClaude Opus 5.5 f6bbb1cfee Batch the Fly App's telemetry and split it per subsystem
The GO 4-style HUD needs far more live data than the bridge sent, and
pushing every SDK callback straight to Dart would rebuild the screen
dozens of times a second. This is the telemetry foundation (plan P1).

On the Kotlin side, every source now writes into a TelemetryHub that
emits one telemetry event per tick: flight-controller state, gimbal and
vision at up to 10 Hz, battery, links, RC, storage and camera settings
folded in at up to 2 Hz. Each event carries the full latest snapshot, so
a consumer that drops packets never loses a slow group. New bridges
cover batteries (every pack, cell voltages, warning thresholds), link
quality and RC battery, diagnostics, and obstacle sensing per sensor.
KeyBridge adds generic getKey/setKey/listenKey over KeyManager for the
settings rows to come. The flight controller also gains smart-RTH
confirmation and simulator start/stop.

On the Dart side, FlightModel now owns one ChangeNotifier per subsystem
(aircraft, battery, links, camera, gimbal, vision, alerts, mission), so
a HUD widget can listen to just the one it draws. TelemetryReducer
routes events into them, only notifies groups that changed, and gives
FlightModel one notification per tick. Flat getters keep the existing
screens working. Diagnostics become graded, de-duplicated alerts.
Settings > About > Telemetry inspector shows every field live and can
start the simulator.

Battery no longer arrives as its own event, so the API Server reads the
percentage from the telemetry snapshot (the old event is still accepted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:34:56 +02:00
..

PilotVault — API Server

Go service that is the single entry point for PilotVault. The Fly App streams drone telemetry to it; the Web App and the built-in API Web Panel read live state and issue commands. It keeps device state in memory and proxies authentication to a PocketBase kept behind it (PocketBase's address is never exposed to clients).

Fly App  ──►  /ws/device  ┐
                          ├─►  API Server (:8080)  ──►  PocketBase (auth only)
Web App  ──►  /ws/ui      ┘

The server root (GET /) serves a PilotVault-branded web panel: a live health readout plus a quick reference of both API audiences. Open http://localhost:8080/ in a browser to check the server at a glance.

The panel is a Vue 3 + Tailwind v4 app in panel/, built into internal/api/dist and embedded into the Go binary at compile time:

cd panel
npm install
npm run build        # outputs to ../internal/api/dist
cd ..; go build -o api-server.exe ./cmd/server   # embeds the fresh dist

For panel development with hot reload (proxies /api to a running server on :8080): cd panel; npm run dev → http://localhost:5174.

Requirements

  • Go 1.24+ (go version)
  • Node 18+ (only for building the panel)
  • A reachable PocketBase instance with a users auth collection (for login). To persist user settings, that collection needs a preferences JSON field — see pocketbase/README.md.

Configure

Copy-Item .env.example .env
# edit .env: set POCKETBASE_URL (and CORS_ALLOW_ORIGINS if needed)
Variable Purpose Default
API_ADDR Listen address :8080
POCKETBASE_URL PocketBase base URL (login proxy). Legacy PB_URL still honoured. http://10.2.1.10:8026
CORS_ALLOW_ORIGINS Comma list, or * *

Run

./scripts/Run-ApiServer.ps1
# or:  go run ./cmd/server

Health check: GET http://localhost:8080/healthz.

API

Client / dashboard endpoints

Method Path Description
GET /healthz, /api/health Readiness probe + device count (public)
POST /api/auth/login {email, password} → PocketBase session (proxied)
GET /api/auth/validate Validate the Authorization token
GET /api/me Caller's {id, email, role, organization, organizationName} resolved from their token
GET /api/preferences Read the caller's saved settings blob (from their PocketBase user record)
PUT /api/preferences {preferences} → persist the caller's settings onto their user record
GET /api/users List users (manager; admin → own org, superadmin → all)
POST /api/users {email, password, role, organization?} → create a user (manager; admin scoped to own org)
PATCH /api/users/{id} Edit {email?, role?, verified?, password?, organization?} (manager; scope-checked; cannot demote self)
DELETE /api/users/{id} Delete a user (manager; admin → own org only; cannot delete self)
GET /api/orgs List organizations (manager; admin → own org, superadmin → all)
POST /api/orgs {name} → create an organization (superadmin only)
PATCH /api/orgs/{id} {name} → rename an organization (superadmin only)
DELETE /api/orgs/{id} Delete an empty organization (superadmin only)
GET /api/admin/pb-config Read the PocketBase connection + a live probe (superadmin only)
POST /api/admin/pb-config/test {url?, adminEmail?, adminPassword?} → probe a candidate connection without applying (superadmin only)
PUT /api/admin/pb-config {url, adminEmail?, adminPassword?} → apply at runtime + persist to .env (superadmin only)
GET /api/admin/plugins List plugins with state + last health (superadmin only)
POST /api/admin/plugins {name, baseURL, provider?} → register an external plugin, no rebuild (superadmin only)
GET /api/admin/plugins/{name} One plugin's view (superadmin only)
PUT /api/admin/plugins/{name} {enabled?, config?} → enable/disable + configure (superadmin only)
DELETE /api/admin/plugins/{name} Remove an external plugin (superadmin only)
POST /api/admin/plugins/{name}/health Run a health check now (superadmin only)
GET /api/devices List devices and their last-known state
GET /api/devices/{id}/track GPS track history for a device
POST /api/devices/{id}/command Send {command, payload?} to a connected device
DELETE /api/devices/{id} Forget a device's stored state
GET /ws/ui Live telemetry stream (WebSocket)

Device endpoints (Fly App)

Method Path Description
GET /ws/device?id={id} Telemetry uplink (WebSocket)
POST /api/telemetry?id={id} Push a single telemetry event over HTTP

Telemetry events

Device messages carry a type: registration, connection, battery, or telemetry (altitude, lat/lng, velocity, GPS sats, flight mode…). The server merges them into a per-device DeviceState and fans each update out to every connected dashboard as {type:"update", device, event}. latitude/longitude samples are appended to the device's GPS track.

Plugins

The server integrates external third-party services through a uniform plugin contract (internal/plugins), managed by a superadmin from the panel. Two kinds share one interface:

  • Built-in — Go connectors compiled into the server (type-safe, first-party). The reference example is OpenSky Network (internal/plugins/builtin/opensky), a live ADS-B flight-state connector with an OAuth2 / anonymous auth provider. Adding a new built-in needs a rebuild.
  • External — a remote HTTP service registered at runtime, no rebuild. It answers a small contract (GET /health, GET /manifest, POST /invoke) and can run as its own process/container (the sandboxing story).

Enable-state and per-plugin config (secrets included) persist to a local, gitignored plugins.json (override with PLUGINS_FILE), loaded on boot. Every plugin exposes a real HealthCheck. Deferred extension points (invocation API, retry/circuit-breaker, per-tenant credentials, audit logging) are documented in internal/plugins/doc.go.

Writing a plugin: see the developer guide internal/plugins/README.md — step-by-step for both built-in (Go) and external (HTTP, no rebuild) plugins, with complete examples.

Project layout

cmd/server/main.go       entry point, wiring, graceful shutdown
internal/config          env/.env configuration
internal/hub             in-memory device state + websocket fan-out (drone core)
internal/api             router, middleware, handlers, embedded panel
internal/plugins         plugin contract, manager, external kind + built-in connectors
panel/                   Vue 3 + Tailwind v4 web panel (built into internal/api/dist)
scripts/                 run helper