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
@@ -371,6 +371,100 @@ func (a *App) handleDeleteOrg(w http.ResponseWriter, r *http.Request) {
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
/* ---------- Logbook: drones ---------- */
|
||||
|
||||
// GET /bff/drones → API Server /api/drones
|
||||
func (a *App) handleListDrones(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/drones", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/drones → API Server /api/drones
|
||||
func (a *App) handleCreateDrone(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/drones", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PATCH /bff/drones/{id} → API Server /api/drones/{id}
|
||||
func (a *App) handleUpdateDrone(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPatch, a.apiBaseFor(r)+"/api/drones/"+url.PathEscape(id), bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// DELETE /bff/drones/{id} → API Server /api/drones/{id}
|
||||
func (a *App) handleDeleteDrone(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
req, _ := http.NewRequest(http.MethodDelete, a.apiBaseFor(r)+"/api/drones/"+url.PathEscape(id), nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
/* ---------- Logbook: flights ---------- */
|
||||
|
||||
// GET /bff/flights → API Server /api/flights
|
||||
func (a *App) handleListFlights(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/flights", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/flights → API Server /api/flights
|
||||
func (a *App) handleCreateFlight(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/flights", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PATCH /bff/flights/{id} → API Server /api/flights/{id}
|
||||
func (a *App) handleUpdateFlight(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPatch, a.apiBaseFor(r)+"/api/flights/"+url.PathEscape(id), bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// DELETE /bff/flights/{id} → API Server /api/flights/{id}
|
||||
func (a *App) handleDeleteFlight(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
req, _ := http.NewRequest(http.MethodDelete, a.apiBaseFor(r)+"/api/flights/"+url.PathEscape(id), nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/logbook/export → API Server /api/logbook/export. Unlike the JSON
|
||||
// endpoints this streams a CSV download, so it preserves the upstream
|
||||
// Content-Type + Content-Disposition instead of forcing application/json.
|
||||
func (a *App) handleExportLogbook(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/logbook/export", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
if cd := resp.Header.Get("Content-Disposition"); cd != "" {
|
||||
w.Header().Set("Content-Disposition", cd)
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
// doRelay executes an outbound request and relays the response verbatim.
|
||||
func (a *App) doRelay(w http.ResponseWriter, req *http.Request) {
|
||||
resp, err := client.Do(req)
|
||||
|
||||
-20
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -35,8 +35,8 @@
|
||||
})()
|
||||
</script>
|
||||
<title>PilotVault — Control Panel</title>
|
||||
<script type="module" crossorigin src="./assets/index-BldP9Pra.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DBe0h801.css">
|
||||
<script type="module" crossorigin src="./assets/index-DLbqB6QP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Co-T5CTN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -72,6 +72,16 @@ func main() {
|
||||
mux.HandleFunc("POST /bff/orgs", app.requireAuth(app.handleCreateOrg))
|
||||
mux.HandleFunc("PATCH /bff/orgs/{id}", app.requireAuth(app.handleUpdateOrg))
|
||||
mux.HandleFunc("DELETE /bff/orgs/{id}", app.requireAuth(app.handleDeleteOrg))
|
||||
// Logbook — drones, flights, and the compliance CSV export (scoping upstream)
|
||||
mux.HandleFunc("GET /bff/drones", app.requireAuth(app.handleListDrones))
|
||||
mux.HandleFunc("POST /bff/drones", app.requireAuth(app.handleCreateDrone))
|
||||
mux.HandleFunc("PATCH /bff/drones/{id}", app.requireAuth(app.handleUpdateDrone))
|
||||
mux.HandleFunc("DELETE /bff/drones/{id}", app.requireAuth(app.handleDeleteDrone))
|
||||
mux.HandleFunc("GET /bff/flights", app.requireAuth(app.handleListFlights))
|
||||
mux.HandleFunc("POST /bff/flights", app.requireAuth(app.handleCreateFlight))
|
||||
mux.HandleFunc("PATCH /bff/flights/{id}", app.requireAuth(app.handleUpdateFlight))
|
||||
mux.HandleFunc("DELETE /bff/flights/{id}", app.requireAuth(app.handleDeleteFlight))
|
||||
mux.HandleFunc("GET /bff/logbook/export", app.requireAuth(app.handleExportLogbook))
|
||||
mux.HandleFunc("GET /bff/ws", app.handleWS)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "api": app.apiBase})
|
||||
|
||||
Reference in New Issue
Block a user