The RelOps Fleet Dashboard β one place to see everything, fix anything, and lose no more sleep over mystery workers.
Hangar pulls data from Taskcluster, SimpleMDM, Puppet, and Google Sheets and stitches it into a single live view of your entire test infrastructure. Pool health, hardware generations, task failures, quarantined machines, missing workers β all in one dark-themed dashboard. No more tab soup.
Currently tracking Mozilla's CI fleet. Built to grow with the rest of the infrastructure.
| Layer | Technology |
|---|---|
| Frontend | React 19 + TypeScript, Vite, Tailwind CSS, Recharts, TanStack Table |
| Backend | FastAPI (Python 3.11), SQLAlchemy 2, APScheduler |
| Database | PostgreSQL 16 |
| Infrastructure | GCP (Cloud Run, Cloud SQL, Cloud Load Balancing, IAP, Cloud Build) |
| Resource | Value |
|---|---|
| URL | https://hangar.relops.mozilla.com |
| GCP Project | relops-dashboard |
| Cloud Run | hangar (us-central1) |
| Auth | Google IAP β @mozilla.com accounts only, no GCP access required |
| CI/CD | Cloud Build triggers on push to main |
# 1. Configure environment
cp .env.example .env
# edit .env β see Environment Variables below
# 2. Start the database
docker compose up -d db
# 3. Build frontend
cd frontend && npm install && npm run build && cd ..
# 4. Start backend (serves built frontend at /)
docker compose up backendOpen http://localhost:8000.
cd frontend
npm install
npm run dev # http://localhost:5173 β proxies API to :8000cd backend
pip install -r requirements.txt
export DATABASE_URL=postgresql://relops:relops@localhost:5432/relops
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# requires: docker compose up -d dbAll variables are optional unless marked required.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql://relops:relops@localhost:5432/relops |
Required. PostgreSQL DSN |
SIMPLEMDM_API_KEY |
β | SimpleMDM REST API key |
TC_ROOT_URL |
https://firefox-ci-tc.services.mozilla.com |
Taskcluster root URL |
TC_CLIENT_ID |
β | TC client ID (public pools work unauthenticated) |
TC_ACCESS_TOKEN |
β | TC access token |
GOOGLE_SHEETS_ID |
β | Master inventory spreadsheet ID |
GOOGLE_EXPORT_SHEET_ID |
β | Export/dashboard spreadsheet ID |
GOOGLE_CREDENTIALS_JSON |
β | Path to service account JSON |
PUPPET_REPO_URL |
https://github.com/mozilla-platform-ops/ronin_puppet |
Puppet repo |
PUPPET_REPO_PATH |
/tmp/ronin_puppet |
Local clone location |
SYNC_INTERVAL_TC |
300 |
Taskcluster sync interval (seconds) |
SYNC_INTERVAL_SIMPLEMDM |
900 |
SimpleMDM sync interval |
SYNC_INTERVAL_SHEETS |
1800 |
Google Sheets sync interval |
SYNC_INTERVAL_PUPPET |
3600 |
Puppet sync interval |
TC_MISSING_THRESHOLD_HOURS |
24 |
Hours before raising a missing_from_tc alert |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β React SPA (Vite) β
β Overview Β· Workers Β· Alerts Β· Pools Β· Consolidationβ
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β REST
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β FastAPI backend β
β /api/workers /api/fleet /api/alerts /api/prs β
β β
β APScheduler βββ sync/taskcluster.py (5 min) β
β βββ sync/simplemdm.py (15 min) β
β βββ sync/google_sheets.py(30 min) β
β βββ sync/puppet.py (60 min) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β SQLAlchemy
ββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββ
β PostgreSQL 16 β
β workers Β· alerts Β· sync_logs Β· failure_events β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
User (mozilla.com Google account)
β
βΌ
Global Load Balancer (34.54.129.77)
β Cloud Armor (OWASP rules + rate limiting)
β Identity-Aware Proxy (IAP) β google auth gate
βΌ
Cloud Run β hangar (us-central1, min 1 instance)
β INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER (direct URL blocked)
βΌ
Cloud SQL β Postgres 16 (private IP via VPC connector)
| Source | What it provides | Sync |
|---|---|---|
| Taskcluster | Worker state, quarantine status, last active, latest task | GraphQL + REST |
| SimpleMDM | MDM enrollment, OS version, serial number, custom attributes | REST (paginated) |
| Puppet | Worker role, pool assignment | Git clone of ronin_puppet |
| Google Sheets | Canonical state (production / loaner / defective / spare / staging), notes | Sheets API v4 |
Your morning briefing. Fleet-wide summary stats, sync health, top-10 failing machines and tests over the last 7 days, generation breakdown chart, workers-by-pool bar chart.
The full roster. Filterable and sortable across generation, state, pool, and MDM/TC status. Search by hostname or serial number. Up to 2,000 rows.
Everything Hangar knows about a single worker β Puppet role, sheet state, MDM enrollment, TC history. Edit notes and jump out to Taskcluster.
The stuff that needs your attention. Types: missing_from_tc, quarantined, mdm_unenrolled, pool_mismatch. Add notes, acknowledge, resolve.
Per-pool health scores, staleness breakdowns (active <24 h / 1β7 d / 7β30 d / 30 d+ / never seen), and job source distribution. Branch-override visibility (counts and which pools/branches are pinned).
Side-by-side hardware generation comparison β state breakdowns, inactive machines, and retirement candidates.
workers β one row per hostname, columns from all four sources
alerts β active/resolved per-worker alerts
sync_logs β audit trail for each sync run (source, duration, records updated, errors)
failure_events β TC task failures indexed by hostname and task name
Worker state precedence: sheet_state (if set) β inferred from TC/Puppet membership β unknown
Health score: fraction of production workers that are MDM-enrolled, not quarantined, and active within 24 hours.
GET /api/workers list + filter + search + sort
GET /api/workers/{hostname} full worker record
PATCH /api/workers/{hostname}/notes update dashboard notes
GET /api/fleet/summary dashboard stats
GET /api/fleet/pools per-pool health
GET /api/fleet/pending-counts TC pending tasks per pool
GET /api/fleet/pool-sources running task project breakdown
GET /api/fleet/failures?days=7 top failing machines + tests
GET /api/fleet/consolidation hardware generation analysis
GET /api/alerts list (filter: type, active_only)
PATCH /api/alerts/{id}/acknowledge
PATCH /api/alerts/{id}/resolve
GET /api/prs/ronin ronin_puppet PR queue + voting
POST /api/prs/ronin/{n}/upvote
POST /api/prs/ronin/{n}/downvote
POST /api/sync/run trigger manual sync
GET /api/health liveness check
hangar/
βββ backend/
β βββ app/
β β βββ main.py FastAPI app, lifespan, route registration
β β βββ config.py Pydantic Settings β all env vars
β β βββ database.py SQLAlchemy engine + table init
β β βββ models.py ORM models
β β βββ api/
β β β βββ workers.py
β β β βββ fleet.py
β β β βββ alerts.py
β β β βββ prs.py ronin_puppet PR voting
β β βββ sync/
β β βββ scheduler.py APScheduler job registration
β β βββ taskcluster.py GraphQL + REST sync, alert generation
β β βββ simplemdm.py Paginated REST sync + custom attributes
β β βββ puppet.py Git clone + inventory.d parse
β β βββ google_sheets.py Sheets API v4 read
β βββ Dockerfile
β βββ requirements.txt
βββ frontend/
β βββ src/
β β βββ App.tsx React Router setup
β β βββ api.ts Typed API client + all TS interfaces
β β βββ components/
β β β βββ Layout.tsx App shell β sidebar nav (collapses to hamburger drawer on mobile)
β β β βββ CommandPalette.tsx βK quick search
β β β βββ KeyboardShortcuts.tsx
β β βββ pages/
β β βββ Overview.tsx
β β βββ Workers.tsx
β β βββ WorkerDetail.tsx
β β βββ Alerts.tsx
β β βββ Pools.tsx
β β βββ Consolidation.tsx
β βββ vite.config.ts Dev proxy β :8000
β βββ package.json
βββ terraform/ All GCP infrastructure
β βββ main.tf Provider, APIs, VPC, VPC connector
β βββ run.tf Cloud Run service
β βββ lb.tf Load balancer, IAP, Cloud Armor, SSL cert
β βββ iam.tf Service accounts, IAM bindings, IAP access
β βββ sql.tf Cloud SQL Postgres 16
β βββ secrets.tf Secret Manager secrets
β βββ variables.tf All configurable vars
β βββ terraform.tfvars Local var overrides (gitignored)
βββ cloudbuild.yaml CI/CD β triggers on push to main
βββ docker-compose.yml Local dev (postgres + backend)
βββ .env.example Template for local dev
| Shortcut | Action |
|---|---|
β K |
Open command palette (jump to any worker) |
β / |
Open keyboard shortcuts help |
Esc |
Close any modal |