src/routes/execution_routes.py and src/routes/asset_routes.py — and both deliberately split their endpoints into two auth zones:
- Drone / agent callbacks (
@check_vpn_ip) — the on-drone uploader and agent call these over the WireGuard plane. There is no JWT; the caller is identified purely by its source IP. - UI endpoints (
@jwt_required()) — the Dashboard reads flight history, triggers analysis, downloads archives, and manages report recipients with a Bearer JWT.
For the auth models in general (JWT Bearer vs VPN-source-IP vs token/signature) and the response-envelope conventions, see HTTP API Overview & Auth Models. The IP-trust mechanics behind
@check_vpn_ip live in VPN IP Authentication & Jumphost Routing. The service-layer internals (log parsing, ZIP building, SMTP) are on Executions, Log Analysis & Reports and S3 Assets, HLS Video & Execution Archives.The flight-data lifecycle
A single flight moves through both auth zones. The drone side (green) writes data during and after the flight; the UI side (blue) reads it back.Drone callbacks (VPN-IP authenticated)
Every handler in this group resolves the calling drone the same way —generic_drone_service.get_drone_by_ip(request.vpn_ip) — where request.vpn_ip is set by the check_vpn_ip decorator from the 10.71.x source IP (or an X-Drone-IP: SKYHUB_SITL_* header for SITL). If no drone matches, the callback returns 404 “Drone not found”.
Execution endpoints
POST /executions/start
POST /executions/start
Starts a
MissionExecution in status in_progress. Body is optional; if mission_id is omitted it falls back to the drone’s currently loaded drone.mission_id (execution_routes.py:172-187).POST /executions/{execution_id}/complete
POST /executions/{execution_id}/complete
Marks the execution finished and computes
duration_seconds. status must be one of completed | aborted | error (default completed); an unknown value returns 400. Ownership is re-verified against the calling drone: the execution must belong to drone.user_id (404 otherwise) and to drone.id (403 otherwise) — see execution_routes.py:237-242.After completion it branches on pending uploads (get_pending_asset_count):- 0 pending →
report_service.enqueue_archive_and_report(execution_id, app)runs the archive + report on a background thread pool. - >0 pending →
mark_report_pending(execution_id); the report is deferred until the lastcomplete_uploadlands (see Deferred reports).
POST /executions/{execution_id}/log
POST /executions/{execution_id}/log
Links an already-uploaded log asset (
asset_id) to the execution and kicks off background pymavlink analysis. It verifies both the execution and the asset belong to the calling drone/user before linking (execution_routes.py:311-321). Analysis only runs if the asset has an s3_key; otherwise it logs a warning and skips.GET /executions/current
GET /executions/current
Returns the drone’s current in-progress execution via
get_in_progress_execution(user_id, drone_id), or {"success": true, "data": null} when idle. The on-drone uploader uses this to discover which execution_id to tag its assets with.Asset upload callbacks
The uploader performs a two-call handshake per file:authenticate_upload mints a presigned S3 PUT URL and (usually) a DB row, the drone uploads directly to S3, then complete_upload flips the asset to ready.
asset_routes.py returns the older raw-JSON envelope here ({"success": true, "url": ..., "asset_id": ...}), not the {success, data} wrapper used by execution routes. It also imports the response helpers from utils.common_helper rather than src.utils.common_helper — both module paths resolve to the same file, so keep both importable.1
authenticate_upload
Body requires The presigned URL expiry is 3 hours (
file_name; asset_type is one of video | image | logs. Pass execution_id to link the asset to a flight (this is what later triggers the deferred report). For non-video assets a checksum is required and is de-duplicated: an already-ready asset with the same checksum returns 409.AssetService.expiration), and the S3 key scheme is drone/{drone_id}/{asset_type}/{file_name} (asset_service.py:169).2
complete_upload
Body is
{ "asset_id": <int> }. The service sets the asset to ready, stamps modified, and generates a thumbnail for images. If the asset carries an execution_id, it then calls _dispatch_pending_report_if_ready(...) which may fire the deferred report.Deferred report logic
Because assets keep uploading after landing, the report cannot always be sent whencomplete fires. The gateway resolves this race with a two-trigger design driven by the MissionExecution.report_pending flag and the count of pending-status assets.
Trigger points, both in code:
complete_execution(execution_routes.py:255-267) — enqueues immediately if nothing is pending, otherwise marks the report pending._dispatch_pending_report_if_ready(asset_routes.py:16-51) — runs after everycomplete_upload; when the execution isreport_pendingandget_pending_asset_count == 0, it callsreport_service.send_report(execution_id)thenclear_report_pending(execution_id).
Async pymavlink log analysis
Attaching a log runsLogAnalysisService.analyze_log_from_s3(s3_key), which downloads the ArduPilot .bin DataFlash log and parses it with pymavlink into a FlightStats dataclass (flight time, distance, altitude/speed, battery, GPS quality, vibration/clipping, mode changes, error subsystems). Results are written to the MissionExecution row via _persist_flight_stats → execution_service.update_flight_stats(...) (~40 stat columns).
There are two entry points with different execution models:
Both paths explicitly catch
SystemExit — pymavlink calls sys.exit() on corrupted logs, which would otherwise kill the worker (execution_routes.py:675, :744). Preserve those guards. The synchronous analyze-log returns stats.to_dict(); if the execution has no linked log asset it returns 400 “No log asset linked to this execution”.UI endpoints (JWT authenticated)
The Dashboard reads flight history and manages reports with a Bearer JWT. Every handler verifies ownership through theuser_id from get_jwt_identity().
Executions
object
Returns
execution.to_dict() merged with drone_name, mission_name, asset_counts, assets (ready, non-log, each enriched with a presigned URL), and log_asset (returned separately, not in the assets array). Only ready assets are counted (execution_service.py:97-145).Reports, archives & flight stats
GET / POST /executions/{id}/archive
GET / POST /executions/{id}/archive
Both delegate to
archive_service.get_archive_download_url(execution_id, regenerate=…). The archive is a ZIP of the execution’s ready assets stored at drone/{drone_id}/archives/*.zip. The presigned download URL and the archive itself are valid for 7 days; the service auto-regenerates when the archive is missing or older than 7 days.GET /executions/{id}/flight-stats
GET /executions/{id}/flight-stats
Reads the persisted columns directly — it does not re-run pymavlink. Returns
flight_time_seconds, total_distance_meters, max_altitude_meters, avg_speed_ms, max_speed_ms, log_analyzed_at, and has_log_asset (execution_routes.py:983-993). If log_analyzed_at is null the log has not been analyzed yet.POST /executions/{id}/send-report
POST /executions/{id}/send-report
Body is
{ "email": "<address>" }, validated against EMAIL_REGEX. Sends the report to that single address via report_service.send_report_to_email(...) (independent of the user’s configured recipient list). The address is masked in logs (_mask_email). Returns 400 on invalid format, 404 if the execution is not owned, 500 if the send fails.GET / PUT /user/report-emails
GET / PUT /user/report-emails
GET returns { email, extra_emails (list), all_recipients }. PUT accepts { "extra_emails": "a@x.com,b@y.com" } (comma-separated); each address is validated with EMAIL_REGEX, normalized, and stored on user.report_extra_emails (set to null when empty). all_recipients combines the account email with the extras.Asset read & download (JWT)
The same asset rows written by the drone callbacks are read back by the UI. These are the HTTP endpoints; the S3 key scheme, HLS rewriting, thumbnails, and two-phase delete are documented in S3 Assets, HLS Video & Execution Archives.GET /assets/<vehicle_id>/video/<asset_id> returns the raw M3U8 body (not the {success, data} envelope) with mimetype="application/vnd.apple.mpegurl", because file:// segment lines are rewritten server-side into presigned .ts URLs so a browser HLS player can fetch them (asset_routes.py:361-397).Where this connects
Service internals
ExecutionService, LogAnalysisService (pymavlink), and ReportService (SMTP + thread pools).Assets & archives
S3 key layout, HLS m3u8 rewriting, thumbnails, and the two-phase delete.
VPN IP auth
How
check_vpn_ip trusts 10.71.x source IPs and X-Drone-IP for SITL.Drone actions
The control commands (arm, takeoff, missions) that bracket an execution.
On-drone uploader
The SkyCore side that calls these callbacks over the VPN.
Cross-system flows
The end-to-end execution + report path across repositories.

