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,91 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GET /api/logbook/export — exports the caller's in-scope logbook as CSV.
|
||||
//
|
||||
// This is the "readable electronic format" disclosure path required by BEK 1649
|
||||
// § 5: retained 5 years and producible on request from Trafikstyrelsen (and,
|
||||
// under the 2026 hearing draft, the police). CSV is an open format, so it holds
|
||||
// regardless of whether the source records came from a manual entry or an
|
||||
// automatic FDR export.
|
||||
func (s *Server) handleExportLogbook(w http.ResponseWriter, r *http.Request) {
|
||||
who := caller(r)
|
||||
drones, err := s.dronesInScope(r.Context(), who)
|
||||
if err != nil {
|
||||
gatewayError(w, err)
|
||||
return
|
||||
}
|
||||
var list struct {
|
||||
Items []flightRecord `json:"items"`
|
||||
}
|
||||
if _, err := s.listRecords(r.Context(), "flights", flightScopeFilter(who),
|
||||
"operation_date,start_time", &list); err != nil {
|
||||
gatewayError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
filename := "pilotvault-logbook-" + time.Now().Format("2006-01-02") + ".csv"
|
||||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
cw := csv.NewWriter(w)
|
||||
defer cw.Flush()
|
||||
|
||||
_ = cw.Write([]string{
|
||||
"operation_date", "start_time", "end_time",
|
||||
"drone_name", "drone_model", "drone_serial", "operator_number",
|
||||
"area_or_route", "max_altitude_agl_m",
|
||||
"remote_pilot", "certificate_ref",
|
||||
"category", "purpose", "logging_path", "fdr_log_url", "authorisation_ref",
|
||||
"weather", "airspace_ref", "observer", "incidents", "notes",
|
||||
"retention_until", "logbook_required", "compliance_flags",
|
||||
})
|
||||
|
||||
for _, f := range list.Items {
|
||||
var d *droneRecord
|
||||
if dr, ok := drones[f.Drone]; ok {
|
||||
d = &dr
|
||||
}
|
||||
c := computeCompliance(f, d)
|
||||
droneName, model, serial, opNo := "", "", "", ""
|
||||
if d != nil {
|
||||
droneName, model, serial, opNo = d.Name, d.Model, d.Serial, d.OperatorNumber
|
||||
}
|
||||
alt := ""
|
||||
if f.MaxAltitudeAGL > 0 {
|
||||
alt = strconv.FormatFloat(f.MaxAltitudeAGL, 'f', -1, 64)
|
||||
}
|
||||
_ = cw.Write([]string{
|
||||
day(f.OperationDate), f.StartTime, f.EndTime,
|
||||
droneName, model, serial, opNo,
|
||||
f.AreaRoute, alt,
|
||||
f.PilotName, f.CertificateRef,
|
||||
f.Category, f.Purpose, c.LoggingPath, f.RawFDRLogURL, f.AuthorisationRef,
|
||||
f.Weather, f.AirspaceRef, f.Observer, f.Incidents, f.Notes,
|
||||
day(f.RetentionUntil), boolText(c.Required), strings.Join(c.RedFlags, "; "),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// day trims a PocketBase datetime string to its YYYY-MM-DD date.
|
||||
func day(s string) string {
|
||||
if len(s) >= 10 {
|
||||
return s[:10]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func boolText(b bool) string {
|
||||
if b {
|
||||
return "yes"
|
||||
}
|
||||
return "no"
|
||||
}
|
||||
Reference in New Issue
Block a user