The Dashboard talks to the Gateway over four transports — HTTP REST, Socket.IO telemetry, the redispad command WebSocket, and Janus WebRTC (see Frontend ↔ Gateway Integration). This page is the REST catalog: it enumerates every HTTP-client service and the exact gateway route each method hits, so you can answer “which service owns POST /drone/action/arm?” without grepping. The realtime services are documented separately — WebsocketService (telemetry), VehicleCommandService / GamepadService / GuidedControlService (redispad commands), and JanusService / AppStateService (video + state). AuthService owns the JWT lifecycle; its endpoints are listed here for completeness only.

Conventions every REST service follows

All services are @Injectable({ providedIn: 'root' }) singletons that take Angular HttpClient in the constructor and build URLs by interpolating ${environment.url}/.... There is no shared typed API-client abstraction — each service hardcodes its route strings. environment.url is http://localhost:5000/api/v1 in dev and https://prod.skyhub.ai:5000/api/v1 in prod (src/environments/environment.ts).
  • Auth: the global AuthInterceptor (src/app/auth/auth.interceptor.ts) attaches Authorization: Bearer <accessToken> to every outgoing request and retries once on 401 by refreshing. No service adds auth headers itself (except AuthService.updatePassword, which passes a temporary token).
  • Response shape is inconsistent — some services get a raw payload, others an ApiResponse<T> envelope ({ data, message? }). See Response envelopes below; getting this wrong is the most common integration bug.
  • drone_id is loosely typed — the Drone model declares id: string, but action payloads send it as string or number interchangeably. The gateway accepts both.

DroneService

src/app/services/drone-service/drone.service.ts — fleet CRUD plus the entire /drone/action/* command family, video-room control, VPN config, and flight-controller parameters. It returns raw payloads (no envelope). Backend counterpart: Drone Management & Control Actions.
MethodHTTPEndpointBody / params
getDrones()GET/drones
getDroneStatus()GET/drones_statusreturns WireGuard handshake/byte counters
createDrone(drone)POST/droneDrone
updateDrone(drone)PUT/drone/{id}Drone
deleteDrone(id)DELETE/drone/{id}
arm(id)POST/drone/action/arm{ drone_id }
takeoff(id, altitude)POST/drone/action/takeoff{ drone_id, altitude }
motorTest(id)POST/drone/action/motortest{ drone_id, motor_id: 1, percentage: 30 }
setMode(id, mode)POST/drone/action/set_mode{ drone_id, mode_info }
move(data)POST/drone/action/moveDroneMovement (SET_POSITION_TARGET fields)
goToGpsLocation(data)POST/drone/action/goto_gps_locationgoto payload
pushMission(id, missionId)POST/drone/action/push_mission{ drone_id, mission_id }
startMission(id, takeoffAltitude?)POST/drone/action/start_mission{ drone_id, takeoff_altitude? }
pushGeofence(id, geofenceId)POST/drone/action/push_geofence{ drone_id, geofence_id }
clearFence(id)POST/drone/action/clear_fence{ drone_id }
syncGeofences(id)POST/drone/action/sync_geofences{ drone_id }
getParams(id)GET/drone/{id}/params
setParam(id, paramId, value)POST/drone/action/set_param{ drone_id, param_id, value }
setParams(id, params)POST/drone/action/set_params{ drone_id, params }
startVideoStream(id)GET/video_room/{id}/start?update=true
stopVideoStream(id)GET/video_room/{id}/stop
restartVideoStream(id)GET/video_room/{id}/restart
getVPNConfig(id)GET/drone/{id}/vpn
getVPNStatus(id)GET/drone/{id}/vpn/status
startMission only includes takeoff_altitude in the body when the argument is defined (drone.service.ts:88). startVideoStream deliberately appends ?update=true — the gateway distinguishes a first-time room creation from a refresh by that query param, so do not drop it. DroneMovement (drone-movement.interface.ts) mirrors a MAVROS SET_POSITION_TARGET_LOCAL_NED message (coordinate_frame, type_mask, position/velocity/accel/yaw).

MissionService

src/app/services/mission.service.ts — mission + waypoint CRUD, returning raw Mission/Mission[]. It defensively coerces mission_points to an array on every read. Backend counterpart: Missions & Geofences API; waypoint semantics live in Mission & Geofence MAVLink Format.
MethodHTTPEndpointNotes
getMissions()GET/missionsarray-coerces mission_points
getMission(id)GET/mission/{id}array-coerces mission_points
creatMission(name)POST/mission{ name } — note the method-name typo
createMissionPoint(points)POST/mission/pointPOI[]
updateMissionPoint(points, id)PATCH/mission/point/{id}POI[]
updateMissionPoints(points, missionId)PATCH/mission/{missionId}/pointsbulk reorder (persists sequence)
deleteMissionPoint(id)DELETE/mission/point/{id}
deleteMission(id)DELETE/mission/{id}
updateCurrentMissionPointIndex(...) is a client-side helper only (no HTTP): it advances the active-waypoint index when the vehicle is within 3 m of the next point, used to redraw the AUTO-mode mission line. See Mission & Waypoint Planning.

GeofenceService

src/app/services/geofence.service.ts — geofence + point CRUD, an enable/disable toggle, and batch point reordering. Returns raw Geofence/Geofence[] (array-coerces geofence_points).
MethodHTTPEndpointBody
getGeofences()GET/geofences
getGeofence(id)GET/geofence/{id}
createGeofence(name, type, fence_type)POST/geofence{ name, type, fence_type, enabled: true }
updateGeofence(id, data)PATCH/geofence/{id}Partial<Geofence>
toggleGeofence(id)PATCH/geofence/{id}/toggle{}
deleteGeofence(id)DELETE/geofence/{id}
createGeofencePoint(points)POST/geofence/pointGeofencePoint[]
updateGeofencePoint(point, id)PATCH/geofence/point/{id}GeofencePoint
updateGeofencePoints(points, geofenceId)PATCH/geofence/{geofenceId}/pointsbatch reorder
deleteGeofencePoint(id)DELETE/geofence/point/{id}
type is 'polygon' | 'circle' and fence_type is 'inclusion' | 'exclusion'. A circle geofence carries a single center point with its radius in param1.

AssetService

src/app/services/asset.service.ts — photo/video assets stored in S3. baseUrl = ${environment.url}/assets. Responses use a { data } wrapper (AssetListResponse, AssetsByVehicleResponse). Backend counterparts: Executions, Assets & Reports API and S3 Assets, HLS Video & Execution Archives.
MethodHTTPEndpointNotes
getAssetList()GET/assets/userper-vehicle photo/video counts
getAssetByVehicleId(id)GET/assets/{id}Asset[] for one vehicle
getVideoAssetByVehicleIdAndAssetId(id, assetId)GET/assets/{id}/video/{assetId}responseType: 'text' — returns an m3u8 (HLS) playlist string
deleteAssets(id, assetIds)POST/assets/{id}/delete{ ids: number[] }
Video assets are HLS: getVideoAssetByVehicleIdAndAssetId fetches the raw .m3u8 playlist as text (not JSON), which the video-player dialog feeds to hls.js. Deletion is a POST .../delete with an ids array — there is no per-asset DELETE route.

ExecutionService

src/app/services/execution.service.ts — flight-execution history, archive downloads, and report-email settings. baseUrl = ${environment.url}/executions. Uses typed *Response models. Backend counterpart: Executions, Log Analysis & Reports.
MethodHTTPEndpointParams / body
getExecutions(limit=50, offset=0)GET/executions?limit&offset
getExecutionsByVehicle(vehicleId, limit, offset)GET/executions/vehicle/{vehicleId}?limit&offset
getExecutionDetail(executionId)GET/executions/{id}
deleteExecution(executionId)DELETE/executions/{id}deletes linked assets too
getArchiveUrl(executionId)GET/executions/{id}/archivegenerates archive if missing
regenerateArchive(executionId)POST/executions/{id}/archive{}
sendReportToEmail(executionId, email)POST/executions/{id}/send-report{ email }
getReportEmails()GET/user/report-emails⚠️ lives under /user, not /executions
updateReportEmails(extraEmails)PUT/user/report-emails{ extra_emails }
The two report-email methods hang off ${environment.url}/user/report-emails (they use the full env URL, not this.baseUrl). If you rename the executions base, these will not move with it.

ScheduledEventService

src/app/services/scheduled-event.service.ts — calendar events with recurrence (RRULE). baseUrl = ${environment.url}/calendar. Every HTTP call unwraps an ApiResponse<T> via .data, and keeps an events$ BehaviorSubject cache. Backend counterpart: Billing, Calendar, VPN, Video & Isaac Sim API.
MethodHTTPEndpointParams / body
getEvents(start, end, droneId?)GET/calendar/events?start&end&drone_id? (ISO dates)
getEventById(id)GET/calendar/events/{id}
createEvent(event)POST/calendar/eventsCalendarEventRequest
updateEvent(id, event)PUT/calendar/events/{id}Partial<CalendarEventRequest>
deleteEvent(id)DELETE/calendar/events/{id}
deleteOccurrence(eventId, occurrenceTime)DELETE/calendar/events/{id}/occurrences?occurrence_time
updateOccurrenceStatus(eventId, time, status, executionId?)PUT/calendar/events/{id}/occurrences/status{ occurrence_time, status, execution_id? }
Recurring events are expanded into “occurrences” client-side. buildRecurrenceRule / parseRecurrenceRule translate the UI’s {type, interval, endDate/endAfter} to and from an RRULE string (FREQ, INTERVAL, UNTIL, COUNT); occurrence edits target the parent event id plus an occurrence_time.

BillingService

src/app/services/billing.service.ts — Stripe subscription and vehicle-limit management. Every call returns ApiResponse<T>. Maintains a subscription$ BehaviorSubject and mirrors active status into localStorage['skyhub_subscription'] via syncSubscriptionCache. Backend counterpart: Stripe Billing & Vehicle Limits.
MethodHTTPEndpointBody / params
getPricing()GET/billing/pricing
getSubscription()GET/billing/subscriptioncaches into subscription$
createCheckoutSession(vehicleCount=1)POST/billing/checkout{ vehicle_count }{ checkout_url, session_id }
createPortalSession()POST/billing/portal{}{ portal_url }
updateVehicleCount(vehicleCount)PUT/billing/subscription/vehicles{ vehicle_count }
cancelSubscription(immediate=false)POST/billing/subscription/cancel{ immediate }
reactivateSubscription()POST/billing/subscription/reactivate{}
getPaymentHistory(limit=20)GET/billing/payments?limit
canAddVehicle(currentCount)GET/billing/can-add-vehicle?current_count
Checkout and portal flows redirect the browser to a Stripe-hosted URL (redirectToCheckout / redirectToPortal set window.location.href). canAddVehicle is the client-side gate before creating a drone; its reason enumerates allowed | no_subscription | subscription_inactive | vehicle_limit_reached. The Stripe publishable key lives in environment.stripePublishableKey (pk_live_xxx) — never a secret key.

IsaacSimService

src/app/services/isaac-sim.service.ts — controls a GPU EC2 instance running NVIDIA Isaac Sim. apiUrl = ${environment.url}/isaac-sim. Gated by the environment.enableIsaacSim feature flag (false by default) and its route is currently commented out in app-routing.module.ts. Polls status every 5 s. Backend counterpart: Users, IP Allocation, Calendar & Isaac Sim.
MethodHTTPEndpointReturns
getStatus()GET/isaac-sim/statusIsaacSimStatus (state, IPs, runtime, cost estimate)
getMetadata()GET/isaac-sim/metadata{ status, data: IsaacSimMetadata }
startInstance()POST/isaac-sim/startIsaacSimOperationResult
stopInstance()POST/isaac-sim/stopIsaacSimOperationResult

UserService

src/app/services/user.service.ts — current-user info and the per-user WireGuard VPN config (distinct from the per-drone VPN endpoints on DroneService).
MethodHTTPEndpoint
getUserInfo()GET/user
getVPNConfig()GET/user/vpn
getUserVPNStatus()GET/user/vpn/status

AuthService (endpoints only)

src/app/auth/auth.service.ts owns login/logout, JWT storage, and the background refresh loop — fully documented in Auth: Guard, Interceptor & AuthService and Authentication & JWT Lifecycle. Its routes, for lookup:
MethodHTTPEndpointBody
login(email, password)POST/login{ username, password }
logout()DELETE/logout
isAuthenticated()GET/authused by AuthGuard
refreshToken()GET/auth/refreshinterceptor swaps in the refresh token
verifyEmail(email)POST/verify-email{ email }
register(token, password)POST/register/{token}{ password }
updatePassword(tempToken, password)POST/update-password{ password } + temp Bearer header
sendPasswordReset(email)POST/reset-password{ email }

Response envelopes

The gateway is not uniform about wrapping responses. Match the pattern per service or response.data will be undefined.
EnvelopeServicesAccess pattern
ApiResponse<T> = { data, message? }BillingService (all), ScheduledEventService (all), AssetService ({ data } wrappers)read response.data
{ status, data }IsaacSimService getMetadataread response.data
Typed *Response modelsExecutionServiceshape defined in execution.model.ts
Raw payload (no envelope)DroneService, MissionService, GeofenceService, IsaacSimService getStatus, UserServiceuse the body directly
responseType: 'text'AssetService getVideoAssetByVehicleIdAndAssetIdm3u8 string, not JSON

Gotchas to preserve

  • No base-URL helper. Every service inlines ${environment.url}/.... Changing the REST path shape (e.g. dropping /api/v1) silently breaks telemetry too, because WebsocketService derives its Socket.IO base from environment.url.replace('/api/v1', '').
  • Report emails are misfiled. ExecutionService.getReportEmails/updateReportEmails call /user/report-emails off the raw env URL, not the executions baseUrl.
  • ?update=true matters. DroneService.startVideoStream always appends it; the gateway treats plain /start and /start?update=true differently.
  • drone_id type drift. Sent as string or number across action payloads; keep the gateway tolerant of both.
  • Isaac Sim ships disabled. The service and components are bundled but the route is commented out and enableIsaacSim is false.
  • Method-name typo. MissionService.creatMission (missing “e”) is load-bearing across callers — rename with care.

Frontend ↔ Gateway Integration

The four transport channels and how ports 5000/7070/8188 map to them.

Auth: Guard, Interceptor & AuthService

JWT storage, Bearer injection, 401 refresh, and the token contract.

Real-time Telemetry Client

WebsocketService: subscribe_telemetry, stream types, RxJS routing.

Vehicle Commands & Gamepad

redispad WebSocket command path, gamepad, guided control.

App State & Video (Janus/WebRTC)

AppStateService store and JanusService video subscription.

Gateway HTTP API Overview

The backend side of every route listed above, with auth models.