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", "drone_registration", "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, reg, opNo := "", "", "", "", "" if d != nil { droneName, model, serial = d.displayName(), d.Model, d.Serial reg, opNo = d.Registration, 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, reg, 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" }