The SkyHub Dashboard is an Angular 16 single-page app built on the classic 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
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch((err: any) => console.error(err));
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.
ProviderSourceRole
HttpClient@angular/common/httpBase REST client used by every service
AuthInterceptor (via HTTP_INTERCEPTORS, multi: true)src/app/auth/auth.interceptor.tsAttaches Bearer tokens, refreshes on 401 — see Authentication
MessageServiceprimeng/apiFeeds the global <p-toast>
ConfirmationServiceprimeng/apiFeeds the global <p-confirmDialog>
Its eager 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
<div class="global-model-progress-indicator" *ngIf="modelProgress.isLoading"></div>
<app-subscription-banner></app-subscription-banner>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>
<router-outlet></router-outlet>
<p-progressBar *ngIf="mainProgressBarLoading" class="main-progress-bar" mode="indeterminate" [style]="{ height: '4px' }" />
ElementDriven byPurpose
3D model download indicatorModelCacheService.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 MessageServiceApp-wide toast notifications
<p-confirmDialog>PrimeNG ConfirmationServiceApp-wide confirmation prompts
<router-outlet>@angular/routerRenders the active route’s component
Main <p-progressBar>ProgressBarService.loading$Thin indeterminate top bar toggled via ProgressBarService.show() / hide()
Beyond hosting chrome, AppComponent (src/app/app.component.ts) does three things in its lifecycle:
1

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.
2

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.
3

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.
Login page

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:
PathModule / ComponentLoadingGuard
''redirect → /homenone
homeHomeModulelazy (loadChildren)AuthGuard
billingBillingModulelazy (loadChildren)AuthGuard
loginLoginComponenteagernone
registerRegisterComponenteagernone
reset-passwordResetPasswordComponenteagernone
preloadingStrategy: PreloadAllModules is set, so the “lazy” home and billing chunks are code-split but eagerly preloaded right after bootstrap — do not assume deferred loading. There is also no wildcard ** route, so unknown URLs render a blank outlet with no 404 page. The full route tree, guard flow, and these gotchas are covered in Routing & Lazy Loading.

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:
RegionElements
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.
Home view with 3D drone and Add Vehicle dialog
MenuMainComponent and MenuTopLeftComponent are declared in the lazy HomeModule, not in AppModule or SharedModule (src/app/home/home.module.ts:103). They exist only inside the home chunk and cannot be reused on the login or billing pages without re-declaration. Likewise, RegisterComponent physically lives under src/app/home/register/ but is declared in AppModule and routed eagerly at /register — a location/ownership mismatch to preserve awareness of during refactors.

Environment & feature flags

Environment selection is compile-time via angular.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:
KeyDefault (environment.ts)Used by
productionfalseGates enableProdMode() in main.ts
httpSessionExpiryTime8 (minutes)AuthService background token refresh timer
Other keys (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.