Files
PilotVault/API Server
tajniak81andClaude Opus 4.8 e9b27530ec Add drone-pilot logbook system (BEK 1649 §5)
Model Denmark's Dronebekendtgørelsen § 5 (on top of EU 2019/947) across all
three tiers: schema, API Server, and Web App.

Schema (migration 1720300700_add_logbook.js): two collections — `drones`
(classification inputs: mtom, is_toy, autologs, c_class, operator no.) and
`flights` (§5 minimum content + category/purpose/logging-path + operational
maturity + a retention_until computed as operation_date + 5y). Locked API
rules; access flows through the service account like users/orgs.

API Server (logbook.go, logbook_export.go): /api/drones and /api/flights CRUD
with per-role scoping in Go (user→own, admin→org, superadmin→all), plus
GET /api/logbook/export (CSV — the "readable electronic format" for
Trafikstyrelsen / pending police disclosure). Compliance is computed
server-side per flight: exemption (toy / club-area / <250 g hobby), effective
logging path, and red flags (autologs-without-FDR, specific-category-without-
authorisation, missing §5 fields, past retention). Manual-path saves missing a
§5 field are blocked (422).

Web App: BFF proxies (export preserves the CSV Content-Type/Disposition),
api.js client fns, and a Logbook.vue view (Flights/Drones tabs, inline forms,
compliance badges + expandable detail, Export CSV) wired into Dashboard.vue,
replacing the placeholder. Includes the rebuilt embedded dist bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:11:39 +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