Split the automatic logging path by the app that logged the flight

logging_path is now manual, automatic_flyapp or automatic_flyagentapp. The
Fly App's entries are backed by the aircraft's flight recorder and keep the
missing-FDR red flag; the Fly Agent App's come from Remote ID, which is not a
flight recorder, so they must carry the § 5 fields like a manual entry. The API
maps the old "automatic" onto automatic_flyapp so Fly App builds already on
devices keep filing. The migration has been applied to the remote PocketBase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-22 19:36:16 +02:00
1 parent 3dd31b257a
commit bc41d8b128
12 files changed
+242 -50

No files matched your search

+52 -13
View File
@@ -175,7 +175,7 @@ type compliance struct {
Required bool `json:"required"` // does § 5 require a logbook entry?
Exempt bool `json:"exempt"` // exempt from the logbook obligation
ExemptReason string `json:"exemptReason"` // why, when exempt
LoggingPath string `json:"loggingPath"` // automatic | manual
LoggingPath string `json:"loggingPath"` // manual | automatic_flyapp | automatic_flyagentapp
RedFlags []string `json:"redFlags"` // compliance gaps to surface
}
@@ -205,16 +205,49 @@ func (f flightRecord) view(drones map[string]droneRecord) flightView {
// Compliance logic (BEK 1649 § 5 + the checklist's red flags).
// ---------------------------------------------------------------------------
// Logging paths: who produced the entry.
//
// manual: typed in by the pilot (Web App form).
//
// automatic_flyapp: recorded by the Fly App from the aircraft's own telemetry
// over the DJI SDK; the FDR log backs it.
//
// automatic_flyagentapp: recorded by the Fly Agent App from the aircraft's
// Remote ID broadcast. That is not a flight data recorder, so the entry must
// carry the § 5 minimum like a manual one.
const (
loggingManual = "manual"
loggingFlyApp = "automatic_flyapp"
loggingFlyAgentApp = "automatic_flyagentapp"
loggingLegacyAutomatic = "automatic" // pre-split value, still sent by older Fly App builds
)
// normalizeLoggingPath maps the pre-split "automatic" onto the Fly App path:
// it was the only app that logged automatically before the split.
func normalizeLoggingPath(p string) string {
p = strings.TrimSpace(p)
if p == loggingLegacyAutomatic {
return loggingFlyApp
}
return p
}
// effectiveLoggingPath is the stored path, or — when blank — derived from the
// drone's capability (autologging → automatic, else manual).
// drone's capability (autologging → the Fly App path, else manual).
func effectiveLoggingPath(f flightRecord, d *droneRecord) string {
if p := strings.TrimSpace(f.LoggingPath); p != "" {
if p := normalizeLoggingPath(f.LoggingPath); p != "" {
return p
}
if d != nil && d.AutologsFlights {
return "automatic"
return loggingFlyApp
}
return "manual"
return loggingManual
}
// needsManualFields reports whether the path has no flight data recorder behind
// it, so the entry itself must hold every § 5 field.
func needsManualFields(path string) bool {
return path == loggingManual || path == loggingFlyAgentApp
}
// manualRequired are the § 5 minimum fields a manual-path entry must carry.
@@ -263,13 +296,13 @@ func computeCompliance(f flightRecord, d *droneRecord) compliance {
c.LoggingPath = effectiveLoggingPath(f, d)
// 3. Red flags (compliance gaps, not just missing data).
if d != nil && d.AutologsFlights && c.LoggingPath == "automatic" && strings.TrimSpace(f.RawFDRLogURL) == "" {
if d != nil && d.AutologsFlights && c.LoggingPath == loggingFlyApp && strings.TrimSpace(f.RawFDRLogURL) == "" {
c.RedFlags = append(c.RedFlags, "Automatic-logging drone but no FDR log stored for this operation")
}
if f.Category == "specific" && strings.TrimSpace(f.AuthorisationRef) == "" {
c.RedFlags = append(c.RedFlags, "Specific-category flight with no linked authorisation reference")
}
if c.Required && c.LoggingPath == "manual" {
if c.Required && needsManualFields(c.LoggingPath) {
for _, m := range missingManualFields(f) {
c.RedFlags = append(c.RedFlags, "Missing § 5 field: "+m)
}
@@ -786,7 +819,7 @@ func (in flightInput) asRecord() flightRecord {
AreaRoute: strings.TrimSpace(in.AreaRoute), MaxAltitudeAGL: in.MaxAltitudeAGL,
PilotName: strings.TrimSpace(in.PilotName), CertificateRef: strings.TrimSpace(in.CertificateRef),
Category: strings.TrimSpace(in.Category), Purpose: strings.TrimSpace(in.Purpose),
LoggingPath: strings.TrimSpace(in.LoggingPath), RawFDRLogURL: strings.TrimSpace(in.RawFDRLogURL),
LoggingPath: normalizeLoggingPath(in.LoggingPath), RawFDRLogURL: strings.TrimSpace(in.RawFDRLogURL),
AuthorisationRef: strings.TrimSpace(in.AuthorisationRef),
}
}
@@ -804,7 +837,7 @@ func (in flightInput) payload(remotePilot, org, retentionUntil string) map[strin
"certificate_ref": strings.TrimSpace(in.CertificateRef),
"category": strings.TrimSpace(in.Category),
"purpose": strings.TrimSpace(in.Purpose),
"logging_path": strings.TrimSpace(in.LoggingPath),
"logging_path": normalizeLoggingPath(in.LoggingPath),
"raw_fdr_log_url": strings.TrimSpace(in.RawFDRLogURL),
"authorisation_ref": strings.TrimSpace(in.AuthorisationRef),
"weather": strings.TrimSpace(in.Weather),
@@ -822,8 +855,9 @@ func (in flightInput) payload(remotePilot, org, retentionUntil string) map[strin
}
// validateFlight enforces the § 5 minimum for the effective logging path. It
// returns an error message (and false) when a manual-path entry is incomplete —
// callers must block the save rather than store a silent partial record.
// returns an error message (and false) when an entry with no FDR behind it is
// incomplete — callers must block the save rather than store a silent partial
// record.
func validateFlight(rec flightRecord, d *droneRecord) (string, bool) {
if strings.TrimSpace(rec.OperationDate) == "" {
return "operation date is required", false
@@ -831,9 +865,14 @@ func validateFlight(rec flightRecord, d *droneRecord) (string, bool) {
if strings.TrimSpace(rec.Drone) == "" {
return "a drone must be selected", false
}
if effectiveLoggingPath(rec, d) == "manual" {
switch effectiveLoggingPath(rec, d) {
case loggingManual, loggingFlyApp, loggingFlyAgentApp:
default:
return "logging path must be manual, automatic_flyapp or automatic_flyagentapp", false
}
if path := effectiveLoggingPath(rec, d); needsManualFields(path) {
if missing := missingManualFields(rec); len(missing) > 0 {
return "manual logbook entry is missing required § 5 field(s): " + strings.Join(missing, ", "), false
return path + " logbook entry is missing required § 5 field(s): " + strings.Join(missing, ", "), false
}
}
return "", true