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>
164 lines
6.4 KiB
JavaScript
164 lines
6.4 KiB
JavaScript
/// <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
|
|
}
|
|
}
|
|
},
|
|
)
|