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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
407e34bf0d
commit
e9b27530ec
@@ -0,0 +1,163 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Creates the drone-pilot logbook collections: `drones` (the airframes an
|
||||
// operator flies, carrying the classification inputs that drive exemption /
|
||||
// logging-path logic) and `flights` (the logbook entries themselves, modelled on
|
||||
// Denmark's BEK nr. 1649 af 12/12/2023 "Dronebekendtgørelsen" § 5).
|
||||
//
|
||||
// Both collections are reached only through the API Server's superuser service
|
||||
// account (like `organizations` + user management), so their API rules are left
|
||||
// locked (superusers only); the API Server enforces per-role scoping in Go.
|
||||
//
|
||||
// Apply by copying into your PocketBase deployment's `pb_migrations/` directory
|
||||
// and restarting. Written for PocketBase v0.22+/v0.23. Idempotent: each
|
||||
// collection is created only if absent, so re-running is a no-op.
|
||||
//
|
||||
// Depends on 1720300200_add_organizations.js (organizations) and the `users`
|
||||
// auth collection.
|
||||
migrate(
|
||||
(app) => {
|
||||
const orgs = app.findCollectionByNameOrId('organizations')
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
|
||||
// ---- drones -----------------------------------------------------------
|
||||
let drones
|
||||
try {
|
||||
drones = app.findCollectionByNameOrId('drones')
|
||||
} catch (_) {
|
||||
drones = new Collection({
|
||||
type: 'base',
|
||||
name: 'drones',
|
||||
fields: [
|
||||
{ name: 'name', type: 'text', required: true, max: 120, presentable: true },
|
||||
{ name: 'model', type: 'text', max: 120 },
|
||||
{ name: 'serial', type: 'text', max: 120 },
|
||||
// Trafikstyrelsen operator number displayed on the drone.
|
||||
{ name: 'operator_number', type: 'text', max: 60 },
|
||||
// Max take-off mass in grams — drives the < 250 g exemption.
|
||||
{ name: 'mtom_grams', type: 'number', min: 0 },
|
||||
{ name: 'is_toy', type: 'bool' },
|
||||
// Has an onboard flight-data recorder (automatic-logging path).
|
||||
{ name: 'autologs_flights', type: 'bool' },
|
||||
// C-class marking: C0..C6 (or blank for legacy/unmarked).
|
||||
{ name: 'c_class', type: 'select', maxSelect: 1, values: ['C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6'] },
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
collectionId: orgs.id,
|
||||
cascadeDelete: false,
|
||||
minSelect: 0,
|
||||
maxSelect: 1,
|
||||
presentable: false,
|
||||
},
|
||||
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
|
||||
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE INDEX `idx_drones_org` ON `drones` (`organization`)',
|
||||
],
|
||||
})
|
||||
app.save(drones)
|
||||
drones = app.findCollectionByNameOrId('drones')
|
||||
}
|
||||
|
||||
// ---- flights ----------------------------------------------------------
|
||||
try {
|
||||
app.findCollectionByNameOrId('flights')
|
||||
return // already present
|
||||
} catch (_) {
|
||||
// create below
|
||||
}
|
||||
|
||||
const flights = new Collection({
|
||||
type: 'base',
|
||||
name: 'flights',
|
||||
fields: [
|
||||
// -- BEK 1649 § 5 minimum content --
|
||||
{ name: 'operation_date', type: 'date', required: true },
|
||||
{ name: 'start_time', type: 'text', max: 5 }, // "HH:MM"
|
||||
{ name: 'end_time', type: 'text', max: 5 }, // "HH:MM"
|
||||
{
|
||||
name: 'drone',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
collectionId: drones.id,
|
||||
cascadeDelete: false,
|
||||
minSelect: 1,
|
||||
maxSelect: 1,
|
||||
presentable: true,
|
||||
},
|
||||
// Area flown or route taken (free text; optional GeoJSON alongside).
|
||||
{ name: 'area_route', type: 'text', max: 500 },
|
||||
{ name: 'route_geojson', type: 'json', maxSize: 200000 },
|
||||
// Maximum altitude relative to terrain, in metres AGL.
|
||||
{ name: 'max_altitude_agl', type: 'number', min: 0 },
|
||||
{
|
||||
name: 'remote_pilot',
|
||||
type: 'relation',
|
||||
required: true,
|
||||
collectionId: users.id,
|
||||
cascadeDelete: false,
|
||||
minSelect: 1,
|
||||
maxSelect: 1,
|
||||
presentable: false,
|
||||
},
|
||||
// Denormalised pilot name — § 5 requires the remote pilot's *name*, which
|
||||
// the users relation alone may not carry.
|
||||
{ name: 'pilot_name', type: 'text', max: 160 },
|
||||
{ name: 'certificate_ref', type: 'text', max: 120 },
|
||||
|
||||
// -- category / logging path --
|
||||
{ name: 'category', type: 'select', maxSelect: 1, values: ['open', 'specific', 'certified'] },
|
||||
// Declared flight purpose — drives the exemption computation.
|
||||
{ name: 'purpose', type: 'select', maxSelect: 1, values: ['hobby', 'commercial', 'research', 'public', 'club_area'] },
|
||||
{ name: 'logging_path', type: 'select', maxSelect: 1, values: ['automatic', 'manual'] },
|
||||
// Link to the stored FDR export (automatic path).
|
||||
{ name: 'raw_fdr_log_url', type: 'text', max: 500 },
|
||||
// Authorisation reference for Specific-category ops (STS/PDRA/SORA).
|
||||
{ name: 'authorisation_ref', type: 'text', max: 200 },
|
||||
|
||||
// -- operational maturity (beyond the legal minimum) --
|
||||
{ name: 'weather', type: 'text', max: 300 },
|
||||
{ name: 'airspace_ref', type: 'text', max: 200 },
|
||||
{ name: 'observer', type: 'text', max: 160 },
|
||||
{ name: 'incidents', type: 'text', max: 1000 },
|
||||
{ name: 'notes', type: 'text', max: 1000 },
|
||||
|
||||
// -- ownership + retention --
|
||||
{
|
||||
name: 'organization',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
collectionId: orgs.id,
|
||||
cascadeDelete: false,
|
||||
minSelect: 0,
|
||||
maxSelect: 1,
|
||||
presentable: false,
|
||||
},
|
||||
// 5-year retention boundary — computed as operation_date + 5y at create.
|
||||
{ name: 'retention_until', type: 'date' },
|
||||
|
||||
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
|
||||
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE INDEX `idx_flights_pilot` ON `flights` (`remote_pilot`)',
|
||||
'CREATE INDEX `idx_flights_org` ON `flights` (`organization`)',
|
||||
'CREATE INDEX `idx_flights_date` ON `flights` (`operation_date`)',
|
||||
],
|
||||
})
|
||||
app.save(flights)
|
||||
},
|
||||
(app) => {
|
||||
// Down: remove flights first (it references drones), then drones.
|
||||
for (const name of ['flights', 'drones']) {
|
||||
try {
|
||||
app.delete(app.findCollectionByNameOrId(name))
|
||||
} catch (_) {
|
||||
// already gone
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user