Commit Graph
38 Commits
Author SHA1 Message Date
tajniak81andClaude Opus 4.8 f2a20e541d Point the root README at the App Key's new home
The Fly App's DJI App Key moved out of android/gradle.properties and into the
developer's ~/.gradle/gradle.properties in 5865553, but the root README's setup
steps still sent people to the old location — where a key would now be both
ignored by the build and committed to git, which is exactly the pair of
outcomes that change existed to prevent.

Audited the rest of the docs and the container stack while here and changed
nothing else: no other README names the key or the old package, and the Fly App
appears across all the Docker files only as a .dockerignore exclusion, which is
correct — it is a Flutter APK built on the developer's machine, not a service in
the stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 21:30:52 +02:00
tajniak81andClaude Opus 4.8 58655531f0 Give the Fly App its own identity instead of the DJI sample's
The app still shipped under the identity of the DJI MSDK Flutter sample it
was started from, so every install, log line and crash report named a DJI
sample rather than PilotVault. Rename both namespaces:

  Android  com.dji.flutter.dji_msdk_sample -> com.pilotvault.flyapp
  Dart     dji_msdk_sample                 -> pilotvault_fly

The Kotlin sources move under com/pilotvault/flyapp to match. The manifest's
meta-data name stays com.dji.sdk.API_KEY — that string is fixed by the SDK's
own lookup and is not ours to rename.

A DJI App Key is bound to the application id, so the old key died with the
old id and a new one was registered against com.pilotvault.flyapp. That
forced the key to be touched anyway, so stop committing it: the Gradle
property becomes PILOTVAULT_FLY_API_KEY and now lives in the developer's
~/.gradle/gradle.properties, which Gradle merges into every build.
android/gradle.properties keeps the build flags — gitignoring it wholesale
would have taken useAndroidX and the Flutter migrator flags out of version
control with it — and documents where the key belongs.

A missing key prints a banner rather than silently baking the placeholder
into an APK that cannot register, which otherwise only surfaces as a
registration failure once the tablet is out at the aircraft. That warning
uses println because `flutter build` filters Gradle's warn-level output.

The rename means Android treats this as a new app: uninstall
com.dji.flutter.dji_msdk_sample before installing, or both will sit on the
tablet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 21:11:17 +02:00
tajniak81andClaude Opus 4.8 6ef15a9e80 Pin container build files to LF via .gitattributes
Docker builds from the working tree, so a CRLF checkout of entrypoint.sh (from
core.autocrlf) would break the shebang inside Alpine and inject \r into the AIO
Dockerfile's heredoc-generated supervisord.conf. Force LF on shell scripts and
Dockerfiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:17:23 +02:00
tajniak81andClaude Opus 4.8 2d20f6f29c Bring the Docker setup in line with DriverVault's
The combined stack now includes PocketBase as a first-class service instead
of assuming an external one: a Docker/pocketbase image built from the release
binary that bundles the pb_migrations and upserts the superuser on boot, so a
fresh stack checks/creates its collections and super-admin the same way
DriverVault does. Add production compose variants (registry images) and
.env(.prod).example files for both Docker/ and Docker AIO/, a top-level compose
name, a PocketBase healthcheck with a service_healthy dependency, and
env-driven ports. Move the containerized PocketBase to :8070 (the live remote
PB stays 10.2.1.10:8026). Add a root .dockerignore to trim the repo-root build
context used by the pocketbase and AIO images.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:16:21 +02:00
tajniak81andClaude Opus 4.8 11fb1e060d List the endpoints the panel had stopped keeping up with
The Overview's tables documented 31 routes where the server serves 53. Everything
added since the plugin cascade went in was simply absent: the logbook, the fleet,
documents, and the integrations surface itself.

Cross-checked against every mux.HandleFunc in server.go, so the only routes left
unlisted are GET /assets/ and GET /favicon.svg — the panel serving itself.

Two judgement calls worth naming:

The integrations triplet is written once as /api/integrations/{plugin} with the
five plugin names in the description, rather than seventeen near-identical rows.
The routes are registered individually, not by wildcard, so that path is an
abstraction the server would not match — flagged in a comment above the list.

/api/health and /api/status are public and belong to no audience in particular.
They sit beside /healthz, which was already in the Device table, retitled
"Device uplink · public" rather than mint a table for three rows.

Logbook/fleet and documents each get their own table, mirroring how the Web App's
nav splits them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:48:00 +02:00
tajniak81andClaude Opus 4.8 183c83c177 Stop reporting the flight controller's serial as the drone's
getSerialNumber() is a BaseComponent method, so every component answers for
itself — and the bridge reads it off the flight controller. A Mavic Pro reports
08RDE1J00103H1 (what DJI Go labels "Flight Controller SN") where the airframe
sticker, and the registration, say 08QDE3H012032E. We were publishing the former
as the drone's serial, onto records that exist to satisfy BEK 1649 §5.

Same trap as 002e484, where a component's own firmware stood in for the
aircraft's, but with no correct source to switch to: MSDK v4 exposes no
aircraft-level serial at all — BaseProduct offers only the model and the
firmware package version — so the registered serial can only be typed by hand.

So split the two rather than pick one:

  serial                    the airframe's, hand-entered, and the only one that
                            reaches the logbook and the CSV export
  flight_controller_serial  what the aircraft reports; auto-filled on connect,
                            and what POST /api/drones/auto now upserts on

Keying auto-add on the flight controller's serial keeps the fleet recognising a
connected drone without typing — it is stable per airframe — while leaving the
compliance record's serial to the pilot. A flight controller swapped in a repair
now costs a duplicate fleet entry to merge, where before it would have quietly
rewritten what the logbook claimed the drone was.

Note droneInput.payload() is a whole-record write, so any UI editing a drone must
round-trip flightControllerSerial; blanking it forks the drone into a duplicate
on its next connect. Drones.vue carries it through the edit form for that reason.

The migration copies existing serials into flight_controller_serial rather than
moving them: every current value came from auto-add and is therefore a flight
controller's, but a pilot may since have corrected one by hand and this cannot
tell them apart. Copying keeps auto-add matching the airframes it matched before.
Applied to the remote PocketBase, where drones held no records, so the backfill
was a no-op there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:36:14 +02:00
tajniak81andClaude Opus 4.8 33595c99e8 Add a Drones fleet section that fills itself in on connect
The fleet lived as a tab inside the Logbook, which buried it, and every
drone had to be typed in by hand — model, serial and firmware copied off
an airframe the app was already talking to.

Promote it to its own nav section above Logbook, and let a connecting
drone register itself. The Fly App already forwarded model, serial and
firmware upstream; the hub was keeping only the model. It now carries the
identity through to DeviceState, and the Web App offers it to a new
POST /api/drones/auto, which upserts keyed by serial. The auto path only
writes what the aircraft is authoritative about (model, both firmware
versions) and never touches what the pilot curates.

Serial and the firmware versions resolve on their own schedules after
connect — the serial in seconds, the aircraft firmware sometimes a minute
later — so nothing along the path treats an absent value as a cleared one,
and a later event filling firmware in still reaches the server. The auto
call rides every telemetry frame, so the client remembers the identity
tuple it last sent and only a change goes out; a 4xx is the server's
settled answer and is not retried, or one drone connected for an hour
would mean one request per frame for an hour.

New fields on drones: firmware, controller_firmware, and registration for
the FAA/CAA aircraft number — distinct from operator_number, which stays
the EU operator ID. Controller firmware is the remote controller's own
version, read from its component; the flight controller's version is a
different quantity and stays off this field (see 002e484). name becomes
optional and is now the pilot's custom name: auto-added drones arrive
unnamed, so the API serves a computed displayName (name, else model +
serial) for the fleet table, the flight picker and the CSV export. A
unique index on serial is what keeps the find-then-create path from
forking a drone's history across two records.

The schema is applied to the remote PocketBase; the migration is here for
fresh deployments, which the remote does not read.

Verified against a simulated device over the real socket with identity
resolving late: one record from four events, both firmware versions
filled, curated fields intact across re-registration, and a drone deleted
while connected coming back on the next frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:04:12 +02:00
tajniak81andClaude Opus 4.8 3e066c0a99 Hide bogus duration badges on Album stills
The Album showed durations like -36:35, -136:23 and 545:26 on a Mavic
Pro's photos. MediaItem.durationLabel only checked for null, but the
SDK fills durationInSeconds for every file, so stills carry junk that
was formatted straight into a badge.

Require isVideo and a positive duration. Stills now get no badge and
nonsense values are rejected; real clips are unaffected. Display-only —
durationLabel renders in one place and nothing else reads it.

Pre-existing, unrelated to the identity work; spotted while testing the
Album against a real aircraft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 15:32:27 +02:00
tajniak81andClaude Opus 4.8 002e484d80 Fix Fly App reporting the flight controller's firmware as the aircraft's
The About panel and home card showed 01.03.0800 on a Mavic Pro whose
firmware is 03.02.35.05, sometimes flipping from the correct value to
the wrong one seconds after connect.

Cause was the component-level fallback I added in ec4b1bc. It is not a
slower route to the same value: BaseComponent.getFirmwareVersion()
reports the component's OWN firmware, so the flight controller answers
01.03.0800 while the aircraft is 03.02.35.05. Because it resolves
quickly and getFirmwarePackageVersion() stays null for far longer than
assumed, the fallback consistently won the race, published a wrong
version, and — since the poll stopped once firmware was non-null — ended
the search for the real one.

Remove it. getFirmwarePackageVersion() is the only source for this
field, so the value can now only be the aircraft's or absent, and the
wrong version has no source in the codebase at all. The field stays
blank until the package version is readable, which is the pre-existing
behaviour and strictly better than showing something wrong.

Widen the poll window to 60s as a best-effort head start now that
nothing fills the gap early. It is not a guarantee: a Mavic Pro was
still null after a minute, and whenever the window expires first the
next connection event carries the version through connectionMap, as it
did before this feature existed.

Found by testing against a real Mavic Pro; two earlier attempts to fix
this by re-ordering the fallback were both disproved on the aircraft.
The serial number this feature added is verified working (08RDE1J00103H1).
This fix is not yet confirmed on hardware — the controller needed charging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 15:32:17 +02:00
tajniak81andClaude Opus 4.8 ec4b1bcdd3 Show drone serial number and firmware in the Fly App
Firmware was already half-wired: the bridge read
BaseProduct.getFirmwarePackageVersion() once, at connect. The SDK
returns null there until it has finished handshaking with the
aircraft, so the About panel almost always showed "-" instead. The
serial was never fetched at all.

Resolve both asynchronously after connect. fetchIdentity() reads the
serial from FlightController.getSerialNumber() and the firmware from
the product package version, falling back to the component-level
getFirmwareVersion() for aircraft that only report the latter. It
re-checks every 2s (max 6 attempts) and emits each value on a new
`identity` event as it lands, so the serial still appears when
firmware never resolves. Values are cached to answer getProductInfo
without re-fetching, and cleared on disconnect.

The SDK delivers lifecycle callbacks on arbitrary threads, so all
identity mutation hops onto the main thread, guarded by a generation
counter that strands retries queued for a product that has since
changed or dropped - otherwise a reconnect could race a stale chain
and report the previous aircraft.

Verified via flutter analyze and an APK build (which type-checks the
new MSDK calls). Runtime timing and the reported values still need a
physical aircraft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:49:57 +02:00
tajniak81andClaude Opus 4.8 42f8054b45 Git-ignore generated DJI SDK class reference
The dji-sdk-classes.html reference is a generated artifact, not source,
so exclude the root-level copy from version control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 22:23:29 +02:00
tajniak81andClaude Opus 4.8 20699fd88d Rebuild Web App embedded frontend bundle
Refresh the Vite build embedded in the Web App server (new hashed
asset filenames + index.html references) after rebuilding the frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:48:44 +02:00
tajniak81andClaude Opus 4.8 a4a2709456 Rebuild Fly App to design v2 with full DJI SDK integration
Rebuild the Flutter/DJI-MSDK-V4 Fly App to the v2 UI kit and wire the
full SDK surface behind it.

Native (Kotlin): split DjiSdkBridge into a method/event router delegating
to per-subsystem SubBridge helpers sharing a BridgeCtx — FlightController
(takeoff/land/RTH + rich telemetry), Camera (mode/record/photo/exposure),
Gimbal, Mission (Waypoint + ActiveTrack; QuickShots via ActiveTrack
QUICK_SHOT), Media (MediaManager list/thumbnail/download), and optional
DJI account login. Manifest gains scoped media permissions.

Flutter: ten screens under lib/ui/ (Flight HUD, capture modes, camera
settings, settings menu, map+waypoints, home, album, academy, profile,
routes/flight logs), driven by an expanded FlightModel. New PVIcon renders
the kit's SVG paths via flutter_svg; map uses flutter_map + latlong2.

Pin transitive androidx.core/browser down to SDK-35-compatible versions
so the newer plugins don't force AGP 8.9.1 onto the DJI toolchain.

Verified with `flutter build apk --debug` (compiles Dart + all Kotlin);
runtime behaviour is untested here — it needs a physical DJI-connected
device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:51:08 +02:00
tajniak81andClaude Opus 4.8 3903428964 Let OpenWeather calls-per-minute limit cascade to orgs and users
Drop the descriptor Default of 60 so the global panel field can be left
blank. A non-blank global value sits at the top of the cascade and locks
every lower layer, so the fixed default made the per-minute limit
un-overridable by organizations and users. Blank now means "inherit";
the runtime still falls back to 60 when no layer sets a value, so the
usage gauge is unchanged. Update the descriptor help and the Settings
row copy to explain the inherit behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:02:31 +02:00
tajniak81andClaude Opus 4.8 5960fb4806 Show current weather on the Overview
Add a weather card to the Web App Overview, wired like the OpenSky live-map
overlay and backed by the OpenWeather plugin.

- API Server: GET /api/integrations/openweather/current resolves the
  caller's cascade, gates it (master/org/personal opt-in/key), and returns
  trimmed current conditions for the configured or a supplied ?lat=&lon=
  point; off/keyless returns {unavailable, detail} so the card degrades.
- BFF relay (forwards the location override) + route; api.js client.
- Dashboard: a Weather card stacked above Schedule showing an emoji
  condition, temperature in the resolved unit, description, location, and a
  feels-like/wind/humidity/cloud grid. Location follows the same cascade as
  the map (drone -> phone -> browser -> default) without prompting for geo;
  refreshes every 10 min while on Overview.
- validLatLon test; rebuilt embedded frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:34:01 +02:00
tajniak81andClaude Opus 4.8 522fe450eb Report keyless OpenWeather as degraded, not down
The panel health probe tests the global plugin config only. When the
plugin is enabled as a master switch with no global API key (users supply
their own), the probe reported a hard "down", which read as broken.

Return "degraded" with an explanatory detail instead — a keyless master
switch is a valid setup, nothing is failing. The Web App user-facing path
is unchanged: a caller whose effective key is empty still gets "down" with
an actionable "add one to connect" message before the plugin is called.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:23:31 +02:00
tajniak81andClaude Opus 4.8 6d73c89cc8 Show OpenWeather API call usage in the app
OpenWeather reports no remaining quota (only 429 when over), so add an
in-app gauge that counts the calls PilotVault itself makes and shows them
against the plan's per-minute limit.

- Plugin: process-wide usage store bucketed per API key (hashed) into the
  current minute and UTC day; a do() wrapper counts each request that
  reaches OpenWeather (transport errors consume no quota, so uncounted).
  Health and Invoke both route through it.
- New callsPerMinute config field (default 60) that cascades global -> org
  -> user; plugin contract gains plugins.HealthUsage on Health.
- Web App: a "Calls per minute limit" field and an "API call usage" meter
  in the OpenWeather card, shown after Test connection and cached.
- Counts reset on restart and cover only PilotVault's own calls (noted in
  the UI); the account dashboard stays authoritative.
- Tests for per-key counting and minute/day rollover; rebuilt frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:49:19 +02:00
tajniak81andClaude Opus 4.8 285551fe8a Let OpenWeather Units fall through the settings cascade
The Units field carried a Default of "metric" and had no blank option, so
the global panel layer always set a value and locked organizations and
users out of changing it. Mirror the fix already applied to OpenSky's plan.

- Plugin descriptor: drop the "metric" Default and add a blank "Not set"
  option; runtime still falls back to metric when no layer sets it.
- Web App: add a "Not set" choice to the Units control and stop forcing
  metric back into the form on load.
- Test guards that the units field has no Default and offers a blank option.
- Rebuilt embedded frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:31:42 +02:00
tajniak81andClaude Opus 4.8 6231b43076 Add OpenWeather plugin with full Web App settings cascade
Builtin API Server connector for the OpenWeather API (current weather +
5-day forecast for a point, API-key auth, stdlib-only), surfaced to end
users through the same three-layer settings cascade as OpenSky/WebDAV.

- New builtin plugin (internal/plugins/builtin/openweather) with
  health probe, weather.current/forecast.5day capabilities, and tests.
- resolveOpenWeather cascade (global -> org -> user) with per-field
  independent resolution and API-key masking; GET/PUT/health endpoints.
- Web App BFF relays, api.js client, and a Settings card under
  APIs - External (scope switch, enable toggles, test connection).
- Resolver unit tests + rebuilt embedded frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:21:48 +02:00
tajniak81andClaude Opus 4.8 816db9b42f Add 5s and 10s Overview Map refresh interval options
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:29:18 +02:00
tajniak81andClaude Opus 4.8 15bff7f517 Add adjustable probe area for OpenSky test connection
Let the OpenSky health probe run over a chosen bounding box without
touching the saved default, so its credit cost (which scales with area)
can be checked cheaply.

- Backend: the plugin health endpoint (panel Check) and the OpenSky
  health endpoint (Web App Test connection) accept an optional ?bbox=,
  validated with validBBox and applied to a transient probe instance;
  both BFF and API forward it. Saved config is untouched.
- API panel: a "Probe area" picker beside each bbox-capable plugin's
  Check button — saved default, all countries (grouped by continent),
  or Custom. Adds a compact pv-input-sm style.
- Web App: a "Test area" picker beside Test connection, using the full
  country list (new countryGroups() helper) plus Custom. This is where
  the probe is authenticated and the credit cost is shown.

Builds pass across both Go modules and both frontends. Panel picker
verified live (186 options, defaults to saved default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 10:25:59 +02:00
tajniak81andClaude Opus 4.8 763cc67081 List all countries in the API panel bounding-box picker
Add API Server/panel/src/countries.js (the full world dataset, mirroring
the Web App's) with a countryGroups() helper, and have the OpenSky bbox
picker render every country grouped by continent instead of just Europe
plus a handful. Panel picker now offers 177 countries (European, Asian,
African, North/South American, Oceanian) alongside World, Continents and
Custom.

Verified live: Plugins → OpenSky → Configure shows the grouped list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 00:17:22 +02:00
tajniak81andClaude Opus 4.8 94f6876024 Add location-aware automatic default bounding box
The OpenSky "Default bounding box" now follows where flying happens.
A new "Automatic" picker mode (the default) resolves the live-map area
from a location cascade — drone telemetry → phone GPS → browser
geolocation → the user's Region country → Europe — instead of a fixed
box. Manual presets and Custom coordinates still work.

- Web App: new shared countries.js dataset (all countries + bbox,
  offline point→country); the bbox picker gains all European countries
  and an Automatic option (client pref prefs.autoBbox); the Region
  setting expands from 6 locale entries to all countries; the live map
  resolves the cascade each poll and sends it as ?bbox=.
- API Server: the states endpoint accepts and validates a ?bbox=
  override (validBBox); the Web App BFF forwards the query; the hub
  relays new phoneLatitude/phoneLongitude telemetry to the Web App.
- Fly App: reports the phone's own GPS (geolocator) alongside
  telemetry, used as the "your location" fallback.
- API panel: the OpenSky bbox picker lists all European countries.

Builds verified across web, panel, both Go modules and the Fly App
APK. Region list, Automatic default and the cascade ?bbox= override
verified in the browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 00:07:12 +02:00
tajniak81andClaude Opus 4.8 150758b0bf Add bounding-box region picker to the API Server plugin panel
Give the superadmin Plugins panel the same OpenSky "Default bounding
box" experience as the Web App: a grouped preset dropdown (World /
Continents / Countries) plus a Custom option for manual
lamin,lomin,lamax,lomax entry.

Introduce a "bbox" config-field type (rendering hint only; unknown
types still degrade to a text input) and mark the OpenSky bbox field
with it. Values that match a preset render as the region name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:28:23 +02:00
tajniak81andClaude Opus 4.8 c5e34e32be Add region picker for OpenSky default bounding box
Replace the free-text bounding-box field in the Web App OpenSky
settings with a grouped preset picker (World / Continents / Countries)
plus a Custom option that keeps the manual lamin,lomin,lamax,lomax
entry. Saved values that match a preset render as the friendly region
name, including in the locked/inherited display.

Change the API Server default bounding box to Europe to match the new
"Europe" preset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:20:08 +02:00
tajniak81andClaude Opus 4.8 6a007fd25e Fix API panel re-injecting field defaults over cleared values
startPluginEdit pre-filled any empty field with its descriptor default,
treating an explicitly cleared field (stored "") the same as one never set
(undefined). So a saved-blank OpenSky bounding box reappeared as the
default 50.5,3.2,53.7,7.3 on reopen (and would be re-persisted on save).
Only inject a default when the field has never been stored (undefined), so
intentional blanks stay blank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:09:46 +02:00
tajniak81andClaude Opus 4.8 125d0ccc7d Make air-traffic refresh interval configurable, resolved from plan
Add a refresh-interval control to the Map settings popover: Auto (follows
the OpenSky plan) or a fixed 15/30/60/120s. The states endpoint now
resolves a recommended interval from the resolved plan (anonymous 60s,
standard 30s, contributor 15s) and returns it alongside the aircraft, so
"Auto" tracks the plan's credit budget and update rate. Polling reschedules
live when the effective interval changes; the map caption reflects it. New
airTrafficInterval preference (default 'auto', persisted + synced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:58:19 +02:00
tajniak81andClaude Opus 4.8 2509fb9c07 Fix Map settings popover rendering under the Live map
The popover sat below Leaflet's map panes and controls (z-index up to
1000), so it was obscured by the map. Lift the control wrapper and popover
to z-index 1200 (with the click-catcher backdrop just under it) so the
menu renders above the map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:39:08 +02:00
tajniak81andClaude Opus 4.8 fee171a71f Add Map settings toggle to show/hide live air traffic
Add a "Map settings" popover (gear button) on the Overview Live map with a
"Show live air traffic" switch, backed by a new client-side showAirTraffic
preference (default on, persisted in localStorage and synced like other
prefs). When off, the aircraft overlay clears and polling pauses to save
OpenSky credits; the count badge and caption reflect the hidden state.
Toggling on re-fetches immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:31:56 +02:00
tajniak81andClaude Opus 4.8 a578555b43 Show live OpenSky air traffic on the Overview Live map
Add GET /api/integrations/opensky/states: resolves the caller's OpenSky
cascade and returns trimmed aircraft state vectors (icao24, callsign,
country, lat/lng, heading, velocity, altitude, onGround) for their bbox.
Reuses the settings gates (global master / org / personal opt-in) and is
backed by a new Manager.InvokeWith that runs the plugin's states.bbox
action on a transient instance. Proxied through the Web App BFF.

DeviceMap now overlays these as rotatable plane markers (accent when
airborne, grey on ground) with tooltips; Dashboard polls every 30s while
Overview is visible and shows an aircraft count badge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:15:18 +02:00
tajniak81andClaude Opus 4.8 97c19083cf Fix Docker build: track web design tokens, anchor Design/ gitignore rule
The repo-root .gitignore rule `Design/` was unanchored, so with Git's
case-insensitive matching on Windows it also caught the Vue source dir
Web App/web/src/design/, leaving tokens.css untracked. Local builds worked
(file on disk) but the Git-derived Docker context failed on
`@import "./design/tokens.css"` (ENOENT).

Anchor the rule to /Design/ so it only excludes the top-level design-assets
folder, and commit the previously-ignored tokens.css source file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:49:23 +02:00
tajniak81andClaude Opus 4.8 28291e27d7 Add in-app document preview
Adds an inline preview for stored document files alongside download.

- API Server: GET /api/documents/{id}/file honours ?inline=1, serving an
  inline Content-Disposition so the browser renders the file instead of
  forcing a download (default stays attachment).
- Web App: BFF forwards the inline flag; documentPreviewUrl() helper; an eye
  icon; and a preview modal (Teleported to body) that picks a viewer from the
  file extension — images -> <img>, PDFs/text -> <iframe>, else a
  download-instead fallback. Closes on backdrop click or Escape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:18:40 +02:00
tajniak81andClaude Opus 4.8 52f84ad6cf Add compliance & operational document store (PocketBase)
Introduces a `documents` collection and full-stack UI for tracking pilot
certificates, aircraft registrations, insurance, airspace authorisations,
contracts, and other paperwork.

- Migration 1720300800_add_documents.js (also provisioned live on remote PB):
  doc_type/owner/expiry/status/access_tier + file blob, a self-referential
  `replaces` version chain, audit fields, and a partial index on expiry_date.
- API Server (documents.go): role-scoped CRUD, server-computed expiry
  assessment, ?expiring=N query, versioning (replaces -> version+1, old row
  auto-archived), and streamed blob download. admin.go gains multipart upload,
  file tokens, and protected-file streaming.
- Web App: BFF passthrough (multipart create + streamed download), api.js
  client fns, and Documents.vue wired into the Documents nav slot.

Blobs live in PocketBase file storage for now; only that backend swaps when
S3-compatible object storage lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:46:18 +02:00
tajniak81andClaude Opus 4.8 e3106d7b60 Show status latency at end of detail line in API panel
Reorder the Status card meta() detail so HTTP status and URL come
first and the latency (ms) reads last, e.g. "HTTP 200 · <url> · 12ms".
Rebuild the embedded panel assets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:36:17 +02:00
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
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
tajniak81andClaude Opus 4.8 407e34bf0d Add project root README
Overview of the PilotVault multi-service project: brand/design pointer,
architecture diagram, surfaces table, PocketBase auth/org data model,
run order (incl. Docker), telemetry lifecycle, and plugin system, with
links to the per-surface READMEs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:52:01 +02:00
tajniak81andClaude Opus 4.8 afc6952eda Initial commit: PilotVault multi-service project
Add API Server (Go/PocketBase), Web App (Go BFF + Vue), Fly App
(Flutter/DJI MSDK), Adobe Plugin, and Docker/Docker AIO deployment
configs. Design assets and build artifacts are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:43:33 +02:00