NgModule architecture (not standalone components). It ships a thin, always-present shell — the bootstrap chain, a set of root providers, and a small band of global UI chrome — that wraps a <router-outlet>. Everything the operator actually flies with lives inside one lazily loaded, monolithic HomeComponent.
This page covers how the app boots, what the shell renders on every page, and how the authenticated experience is composed. Routing rules, the auth guard/interceptor, backend transports, and telemetry each have their own dedicated pages — see the cross-links at the bottom.
Bootstrap chain
Startup is deliberately minimal.src/main.ts enables prod mode only when the compiled environment says so, then bootstraps the root module:
src/main.ts
AppModule (src/app/app.module.ts:23) declares AppComponent as its bootstrap component. The <app-root> tag in src/index.html:89 is where Angular renders it (the page <title> is SkyHub Δ).
AppModule: root providers & declarations
AppModule is the composition root for cross-cutting concerns. It wires the HTTP stack, registers the single HTTP interceptor, and provides the PrimeNG services that the global chrome consumes.
| Provider | Source | Role |
|---|---|---|
HttpClient | @angular/common/http | Base REST client used by every service |
AuthInterceptor (via HTTP_INTERCEPTORS, multi: true) | src/app/auth/auth.interceptor.ts | Attaches Bearer tokens, refreshes on 401 — see Authentication |
MessageService | primeng/api | Feeds the global <p-toast> |
ConfirmationService | primeng/api | Feeds the global <p-confirmDialog> |
declarations are the entire unauthenticated surface plus one orphan:
AppComponent, LoginComponent, RegisterComponent, ResetPasswordComponent, GamepadComponent.
GamepadComponent is declared here but its selector app-gamepad is never used in any template — it is a dead declaration. Actual controller input is handled by GamepadService and ControllerDataSenderService, not this component (src/app/core/gamepad/gamepad.component.ts).SharedModule (src/app/shared/shared.module.ts) is imported by both AppModule and HomeModule. It declares and re-exports SafePipe, ShrinkNamePipe, MainLogoComponent, and SubscriptionBannerComponent, and re-exports CommonModule + FormsModule so feature modules don’t re-import them.
Global chrome (AppComponent)
AppComponent renders a fixed band of app-wide UI around the router outlet. Its template (src/app/app.component.html) is intentionally tiny:
src/app/app.component.html
| Element | Driven by | Purpose |
|---|---|---|
| 3D model download indicator | ModelCacheService.progress$ | Shows Loading 3D Model: N% while the observer.glb drone model streams from S3 into IndexedDB |
<app-subscription-banner> | BillingService.subscription$ | Upgrade prompt shown when the user has no active/trialing subscription; hidden on /login, /register, /reset-password; dismissible for 24h via localStorage key subscription_banner_dismissed_until |
<p-toast> | PrimeNG MessageService | App-wide toast notifications |
<p-confirmDialog> | PrimeNG ConfirmationService | App-wide confirmation prompts |
<router-outlet> | @angular/router | Renders the active route’s component |
Main <p-progressBar> | ProgressBarService.loading$ | Thin indeterminate top bar toggled via ProgressBarService.show() / hide() |
AppComponent (src/app/app.component.ts) does three things in its lifecycle:
Configures PrimeNG globally (constructor)
Enables
ripple and sets the z-index stack (modal: 1200, overlay/menu: 1000, tooltip: 1100) so dialogs, menus, and toasts layer correctly.Subscribes to the two progress streams (ngOnInit)
Binds
ProgressBarService.loading$ to the top bar and ModelCacheService.progress$ to the 3D-model indicator, calling cdr.detectChanges() for the former.Initializes the command sender (ngOnInit)
Calls
controllerDataSenderService.init() to bring up the redispad command channel. Gamepad/keyboard listeners are deliberately not started here — gamepadService.initGamepad() is deferred to HomeComponent.ngOnInit (src/app/home/home.component.ts:260) so the login page never attaches keyboard handlers.
Route structure at a glance
The root route table (src/app/app-routing.module.ts) splits the app into an eager auth surface and two lazy, AuthGuard-protected feature modules:
| Path | Module / Component | Loading | Guard |
|---|---|---|---|
'' | redirect → /home | — | none |
home | HomeModule | lazy (loadChildren) | AuthGuard |
billing | BillingModule | lazy (loadChildren) | AuthGuard |
login | LoginComponent | eager | none |
register | RegisterComponent | eager | none |
reset-password | ResetPasswordComponent | eager | none |
The monolithic HomeComponent
The entire authenticated dashboard — map, fleet, missions, geofences, video, telemetry — is rendered by a single ~120 KB god component,HomeComponent (src/app/home/home.component.ts). It is the only routed component in HomeModule’s child routes ({ path: '', component: HomeComponent }, src/app/home/home-routing.module.ts).
HomeComponent.ngOnInit boots the operational UI: it locks body scroll, shows the progress bar, initializes the Mapbox map and Threebox 3D layer, starts user-location tracking, initializes the gamepad, opens the telemetry WebSocket, and subscribes to AppStateService.selectedDrone$.
Its template (src/app/home/home.component.html) is the composition root of the authenticated UI:
| Region | Elements | |
|---|---|---|
| Navigation | <app-menu-main> (left profile sidebar), <app-menu-top-left> (top-left megamenu) | |
| Map surface | #projected-route-map container, <app-drone-status>, <app-quick-buttons>, <app-mission-control>, <app-terminal>, <app-virtual-joystick> | |
| Side panels | <app-edit-mission>, <app-edit-geofence>, <app-video-window> | |
| Dialogs | ~40 dialog components, each gated by `*ngIf=“dialogService.isDialogActive(‘id’) | async”` |
Dialogs are opened by string id through
DialogService (src/app/services/dialog.service.ts). showDialog('app-list-drones-dialog') flips a BehaviorSubject; the template’s isDialogActive(id) | async mounts the component only while it’s open (and destroys it on close). isDialogActive returns a shareReplay-cached observable per id so the async pipe keeps a stable reference across change detection. This id registry is what couples the menus to their dialogs.
Environment & feature flags
Environment selection is compile-time viaangular.json fileReplacements — there is no runtime config. The default src/environments/environment.ts is swapped for .prod / .aws-dev / .e2e / .local per build configuration. The shell itself reads only two values directly:
| Key | Default (environment.ts) | Used by |
|---|---|---|
production | false | Gates enableProdMode() in main.ts |
httpSessionExpiryTime | 8 (minutes) | AuthService background token refresh timer |
url, ws_proxy, janusGatewayUrl, assetsUrl, enableIsaacSim, stripePublishableKey, mapbox) are consumed downstream by services and feature components, not by the shell. Note the Stripe publishable key in the env files is a live pk_live_xxx value and Mapbox/Stripe values are identical across dev and prod files. Build configurations, service-worker flags, and bundle budgets are detailed in Environments, Build & CI.
Where to go next
Routing & Lazy Loading
The full route tree,
PreloadAllModules behavior, and the missing 404 route.Auth: Guard, Interceptor & AuthService
AuthGuard, Bearer injection, 401 refresh-and-retry, and the token lifecycle.Frontend ↔ Gateway Integration
The four transport channels (REST, Socket.IO, redispad WS, Janus) and their ports.
Angular Services & REST Reference
Catalog of every root-provided service and the gateway endpoints it calls.

