Files
PilotVault/API Server
tajniak81andClaude Opus 4.8 09da889c15 Add Users and Organizations management to API Server panel
The superadmin panel had backend CRUD for users/orgs (internal/api/users.go,
orgs.go) but no UI. Add two tabs to panel/src/App.vue:

- Users: create form (email/password/role/org), list with role + org badges,
  inline editor (email/role/org/verified/password reset), and delete. Self-role
  change and self-delete are suppressed to mirror the server-side guards.
- Organizations: create, inline rename, and delete, with client-derived member
  counts; delete is disabled while an org still has members.

PocketBase validation errors are unwrapped into a readable line via a shared
apiError() helper. Rebuilds the embedded panel bundle (dist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:29:50 +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 devhttp://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