# 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/`](panel/), built into `internal/api/dist` and embedded into the Go binary at compile time: ```powershell 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`](pocketbase/README.md). ## Configure ```powershell 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 ```powershell ./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`](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 ```