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.
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.
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.
Method
HTTP
Endpoint
Body / params
getDrones()
GET
/drones
—
getDroneStatus()
GET
/drones_status
returns WireGuard handshake/byte counters
createDrone(drone)
POST
/drone
Drone
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/move
DroneMovement (SET_POSITION_TARGET fields)
goToGpsLocation(data)
POST
/drone/action/goto_gps_location
goto 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).
src/app/services/mission.service.ts — mission + waypoint CRUD, returning rawMission/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.
Method
HTTP
Endpoint
Notes
getMissions()
GET
/missions
array-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/point
POI[]
updateMissionPoint(points, id)
PATCH
/mission/point/{id}
POI[]
updateMissionPoints(points, missionId)
PATCH
/mission/{missionId}/points
bulk 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.
src/app/services/geofence.service.ts — geofence + point CRUD, an enable/disable toggle, and batch point reordering. Returns rawGeofence/Geofence[] (array-coerces geofence_points).
Method
HTTP
Endpoint
Body
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/point
GeofencePoint[]
updateGeofencePoint(point, id)
PATCH
/geofence/point/{id}
GeofencePoint
updateGeofencePoints(points, geofenceId)
PATCH
/geofence/{geofenceId}/points
batch 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.
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.
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.
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.
Method
HTTP
Endpoint
Params / body
getEvents(start, end, droneId?)
GET
/calendar/events
?start&end&drone_id? (ISO dates)
getEventById(id)
GET
/calendar/events/{id}
—
createEvent(event)
POST
/calendar/events
CalendarEventRequest
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.
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.
Method
HTTP
Endpoint
Body / params
getPricing()
GET
/billing/pricing
—
getSubscription()
GET
/billing/subscription
caches 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.
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.
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).
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.