The SkyHub Dashboard is an Angular 16 single-page app (package name skygridmap) compiled with the esbuild browser builder into a flat dist/ bundle, then packaged into an nginx container. There is no runtime configuration: the environment (which Gateway URL, which Janus/WS-proxy endpoints, Stripe key, prod-mode flag) is chosen at build time by swapping src/environments/environment.ts for one of five variants via Angular file replacements. Pick the wrong build configuration and the SPA silently points at the wrong backend. This page covers how those environments map to build configurations, the container image, and the CI/deploy pipeline. For what the app does with these values at runtime, see App Shell & Bootstrap, Frontend ↔ Gateway Integration, and App State & Video.

Environments and build configurations

Every build configuration in angular.json is defined for the single project dashboard. The production configuration is the default (defaultConfiguration: "production" at angular.json:154), so a bare ng build / npm run build produces a production artifact.
ConfigurationEnv file (replaces environment.ts)url (Gateway REST)productionOptimizedNotes
production (default build)environment.prod.tshttps://prod.skyhub.ai:5000/api/v1trueyes, outputHashing: allBudgets enforced; live Stripe key
aws-devenvironment.aws-dev.tshttps://dev.skyhub.ai:5000/api/v1trueyes, outputHashing: allBudgets enforced; no Stripe
development (default serve)environment.ts (unchanged)http://localhost:5000/api/v1falseno, sourcemaps + named chunksng serve default
localenvironment.local.tshttp://localhost:5000/api/v1falseno, sourcemapsserviceWorker: false; all endpoints localhost
e2eenvironment.e2e.tshttps://prod.skyhub.ai:5000/api/v1trueinherits baseBacks Playwright’s webServer
The development configuration does not replace the environment file — it uses environment.ts as-is. That file is the checked-in default, and it carries production: false together with the live Stripe publishable key and the production Janus / WS-proxy / assets URLs (src/environments/environment.ts:11-19). Only url (localhost:5000) is dev-flavored. If you add a new endpoint, remember to set it in every one of the five files, not just environment.prod.ts.

Environment variable reference

All five files export the same object shape. The base object lives in src/environments/environment.ts; the per-environment overrides differ only in the fields below.
KeyPurposeProd valueLocal value
urlGateway REST base (/api/v1)https://prod.skyhub.ai:5000/api/v1http://localhost:5000/api/v1
productionAngular prod-mode flag (enableProdMode())truefalse
janusGatewayUrlJanus WebSocket for WebRTC videowss://prod.skyhub.ai:8188ws://localhost:8188
janusIceServersSTUN/ICE servers['stun:stun.l.google.com:19302']same
ws_proxyGamepad WebSocket proxy (redispad)wss://prod.skyhub.ai:7070ws://localhost:7070
assetsUrlS3 base for drone assetshttps://skyhub-prod-assets.s3.eu-central-1.amazonaws.com/dronesame (all envs)
mapbox.accessTokenMapbox GL public tokencommitted pk.eyJ1... (same all envs)same
stripePublishableKeyStripe billing UI keypk_live_xxx (present only in environment.ts + environment.prod.ts)absent
enableIsaacSimIsaac Sim feature flagfalse (all envs)false
sseDebounceTimeStream-update debounce (ms)10001000
httpSessionExpiryTimeClient session expiry (minutes)88
DEFAULT_LAT / DEFAULT_LNGInitial map center (Plovdiv, BG)42.1354 / 24.7453same
aws-dev is the outlier that routes the gamepad proxy through Cloud Map service discovery: ws_proxy: 'wss://ws_proxy.skyhub-dev.internal:7070' (src/environments/environment.aws-dev.ts:15). The janusGatewayUrl, ws_proxy, and url all encode the jumphost/domain the browser must reach — see VPC, Jumphost & nginx Routing, Janus SFU, and WebSocket Gamepad Proxy.
A live Stripe publishable key and the Mapbox token are committed in source. Publishable keys are lower-risk (not secret keys), but they are checked-in and shipped in the bundle. When rotating billing keys, update both environment.ts and environment.prod.ts. See Stripe Billing.

npm scripts

package.json requires Node >= 24 and pins packageManager: [email protected], but the Dockerfile uses npm (see the drift note below).
ScriptCommandWhat it does
npm startng serveDev server, development config (localhost:5000), port 4200
npm run start:localng serve --configuration=local --host 0.0.0.0 --openAll-localhost endpoints, SW disabled, LAN-accessible
npm run start:e2eng serve --configuration=e2e --host 0.0.0.0Serves the e2e config; used as Playwright’s webServer
npm run buildng buildProduction build (default config) → flat dist/
npm run build:localng build --configuration=localUnoptimized, SW-off build
npm run watchng build --watch --configuration developmentRebuild on change
npm testjestUnit tests (jsdom, serial)
npm run test:coveragejest --coverage --coverageProvider=v8Coverage → coverage/
npm run playwright[:ui|:headed|:debug|:report]playwright test ...E2E run / inspect
npm run biome:lint / biome:check / biome:check:writebiome ...Lint / format-check / auto-fix (CI gate)
npm run format / format:checkprettier ...Prettier over src/** (local only)
To build the AWS-dev flavor there is no npm alias — invoke the CLI directly:
ng build --configuration aws-dev

Bundle budgets and output layout

The production and aws-dev configurations enforce bundle budgets (angular.json:84-95):
Budget typeWarningError
initial2 MB2.5 MB
anyComponentStyle50 KB80 KB
outputPath is dist — a flat layout (angular.json:47). The esbuild browser builder writes index.html, main.[hash].js, styles.[hash].css, ngsw.json, and ngsw-worker.js directly under dist/, not the Angular-typical dist/<project>/browser. The Dockerfile’s COPY --from=build /usr/local/app/dist /usr/share/nginx/html depends on this. A builder upgrade that introduces a dist/browser/ subdirectory will break the container (blank page / 404s) until the COPY path is updated.
Global styles (PrimeNG light-blue theme, Mapbox GL, FontAwesome, threebox) and scripts (adapter.js, janus.js) are declared in angular.json:68-78. CommonJS deps that would otherwise warn (mapbox, lodash, nanoid, etc.) are whitelisted in allowedCommonJsDependencies. Target browsers come from .browserslistrc (last 1 Chrome/FF, last 2 Edge/Safari/iOS, Firefox ESR, no IE11).

Container image (two-stage, nginx on 8001)

1

Stage 1 — build (node:latest)

Dockerfile:4-16 runs npm install then npm run build with no arguments, so it produces the production configuration (prod.skyhub.ai). Output lands in the flat dist/.
2

Stage 2 — serve (nginx:latest)

Dockerfile:22-27 copies nginx/nginx.conf to /etc/nginx/nginx.conf and dist/ to /usr/share/nginx/html. The SPA is served with a fallback so deep links resolve to index.html.
3

Run

Build and run:
docker build . -t skyhub_dashboard
docker run -p 8080:8001 skyhub_dashboard   # host:container — container listens on 8001
The active nginx config (nginx/nginx.conf) is minimal:
nginx/nginx.conf
worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;

    server {
        listen 8001;
        root /usr/share/nginx/html;
        index index.html;

        location / {
            try_files $uri $uri/ /index.html;   # SPA fallback
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}
Port mismatch: Dockerfile:29 declares EXPOSE 80, but nginx/nginx.conf listens on 8001. The container really serves on 8001 — the EXPOSE line is cosmetic. Whatever orchestrator or reverse proxy fronts this image must target 8001. See VPC, Jumphost & nginx Routing for how the prod jumphost proxies to it.
A second nginx server block, production.conf, exists at the repo root with a different SPA fallback (try_files $uri $uri/ /index.html?$args). It is never referenced by the Dockerfile — only nginx/nginx.conf is baked in. Ignore production.conf or delete it; the two divergent fallback rules are a trap for future editors.
The Dockerfile uses npm install on node:latest (unpinned), while the repo declares packageManager: [email protected], engines.node >= 24, and CI pins node 24.11.1 and installs with yarn install (--frozen-lockfile in the Playwright/Biome workflows). A local docker build can resolve a different dependency tree than CI (no lockfile enforcement, npm vs yarn, floating base image).
There is no .dockerignore, so COPY . /usr/local/app/ pulls node_modules, dist, .git, coverage, and playwright-report into the build context and the stage-1 layer, bloating build time and image size.

The service worker (built but unregistered)

angular.json:79-80 sets serviceWorker: true with ngswConfigPath: ngsw-config.json, so every non-local build emits ngsw-worker.js and ngsw.json into dist/. ngsw-config.json defines two asset groups: app (prefetch: index.html, CSS, JS, manifest) and assets (lazy, prefetch update mode, excluding the demo/, demo_mission_history_assets/, and theme directories).
The service worker is never registered at runtime. There is no ServiceWorkerModule.register(...) and no navigator.serviceWorker.register(...) anywhere in src/ (verified by grep). The offline-caching runtime is dormant — the emitted ngsw-worker.js is dead weight. Only src/manifest.webmanifest is live: it enables install / add-to-homescreen (name SkyHub, display: standalone, maskable icons 72–512 px), wired via src/index.html (<link rel="manifest">, theme-color #317EFB, Apple PWA meta).A future refactor must decide: either wire ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) in the app bootstrap, or drop the SW machinery entirely. Note the local config sets serviceWorker: false while production/aws-dev/e2e leave it on — so caching behavior would differ by config the moment someone registers it.

Testing

jest.config.json uses preset: jest-preset-angular on a jsdom environment, runs serially (maxWorkers: 1) for deterministic results, and collects coverage (json, lcov, text, clover). Native/WebGL deps are mocked via moduleNameMapper so tests don’t touch a real GPU:
jest.config.json
"moduleNameMapper": {
  "^src/(.*)$": "<rootDir>/src/$1",
  "^three/examples/jsm/loaders/GLTFLoader\\.js$": "<rootDir>/src/app/services/__mocks__/GLTFLoader.mock.ts",
  "^three$": "<rootDir>/src/app/services/__mocks__/three.mock.ts"
}
Global setup (setup.jest.ts) mocks mapboxgl, DragEvent, window.CSS/getComputedStyle. Removing or renaming the mocks under src/app/services/__mocks__ silently breaks the suite.
npm test              # run once
npm run test:watch    # watch mode
npm run test:coverage # -> coverage/
Stale docs: the dashboard repo’s own README.md and CLAUDE.md still describe E2E as Cypress (npm run cypress:open), and .nycrc targets coverage/cypress. No Cypress config or scripts exist — the project uses Playwright. Ignore those references.

Continuous integration

Three GitHub Actions workflows all run on push/PR to development and main on Node 24.11.1, installing deps with yarn.
WorkflowStepsArtifacts
jest_unit_tests.ymlyarn installnpm testnpm run test:coveragecoverage/
playwright.ymlyarn install --frozen-lockfileplaywright install chromium --with-depsnpm run playwrightplaywright-report/, test-results/
biome.ymlbiome-check job: yarn biome:lint + yarn biome:check. biome-auto-fix job (PR only): biome:check:write, then commits fixes back to the PR branch and comments
Branch naming is inconsistent across the repo: CI triggers on development, but .coderabbit.yaml lists develop/main/master as base branches. Production releases build from the development branch (see below).

Production release (AWS CodeBuild)

Production releases are not driven from this repo’s workflows. They run out-of-repo on an AWS CodeBuild project named skyhub-prod-frontend-build-and-deploy in eu-central-1, building from the development branch on Node 24.11.1. The buildspec lives in the CodeBuild project, not the repo.
aws codebuild start-build \
  --project-name skyhub-prod-frontend-build-and-deploy \
  --region eu-central-1
CodeBuild invokes two committed Slack helpers around the build — slack/notify_start.js (posts :rocket: Started) and slack/notify_finish.js (posts SUCCESS/FAILURE from CODEBUILD_BUILD_SUCCEEDING). Both read CodeBuild env vars: SLACK_WEBHOOK_URL, PROJECT_NAME, COMMIT_HASH, TAG, CODEBUILD_BUILD_ID, CODEBUILD_SOURCE_VERSION, AWS_REGION, CODEBUILD_LOG_PATH. For how the built static bundle is served and fronted in production, and the parallel Gateway build pipeline, see CI/CD: CodeBuild, ECR & Frontend Deploy and Gateway Build, Docker & Runtime.

Adding a new environment or endpoint

1

Add the field to all five environment files

Add the key to environment.ts (the base/default) and to environment.prod.ts, environment.aws-dev.ts, environment.local.ts, environment.e2e.ts. TypeScript’s structural typing means an object missing a field a component reads will fail to compile only where it’s used — keep the shape uniform.
2

(New environment only) add a build configuration

Add a configurations.<name> block in angular.json with a fileReplacements entry pointing environment.ts → your new file, plus budgets/hashing to match production if it’s an optimized build. Add a matching serve browserTarget if it needs a dev server.
3

Add an npm script (optional)

Mirror the existing pattern, e.g. "build:staging": "ng build --configuration=staging".
4

Run Biome before committing

npm run biome:check:write — CI will auto-fix on the PR otherwise, adding a bot commit.