An internal operations dashboard for a GPU fleet — and a capacity allocation planner that shows its whole reasoning.
Hosted on a free instance, so the first request after a quiet spell cold-starts and can take up to a minute. Everything after that is immediate.
ComputeOps monitors capacity, utilization, power, operating cost, customer workloads, and incidents across five data centers, then answers the question an operations team gets asked every week: a customer wants 64 H100s for six months — where should they go, and why?
Every number in this project is generated, not measured. The data centers, customers, workloads, and incidents are fictional and derived deterministically from a fixed seed. ComputeOps never submits, schedules, or executes a GPU job, and never contacts real infrastructure — the backend contains no subprocess calls, no container or Kubernetes clients, no SSH, and no cloud SDKs, and a test enforces that by auditing the source. The allocation engine recommends and never acts.
This is a portfolio project, inspired by the kinds of problems that come up in AI infrastructure operations. It is not a record of operating a real fleet, and nothing here should be read as production experience.
Fleet Overview — the “is everything alright?” page.
A GPU fleet is not a pool of interchangeable capacity. Each site has a different electricity price, a different cooling efficiency, a different reliability record, different compliance certifications, and a different amount of headroom left. Placing a customer badly is expensive in ways that surface months later: a site that was cheap on paper turns out to have been at 94% utilization, or the only site with EU data residency turns out to be the one that was thermally stressed all summer.
In practice the decision is usually made in a spreadsheet, by the person who knows the fleet best, and the reasoning lives in their head. That creates three problems:
- It doesn't scale past that person. Sales asks for a placement, capacity planning answers, and nobody else can reproduce the answer.
- It can't be audited. When a placement turns out badly, there's no record of which factors were weighed, or how heavily.
- The policy can't be changed deliberately. If the company decides cost matters less than reliability this quarter, there is nothing to edit — only a habit to argue with.
ComputeOps is a small, concrete answer to all three: monitor the fleet, encode the placement policy in a file a non-engineer can read, and make every recommendation explain itself.
Headline KPIs — fleet size, allocation-weighted utilization, power draw against provisioned capacity, operating cost, PUE, and open incidents — over a utilization-by-site chart with an incident rail that explains the dips. Below it: capacity distribution across sites, per-site detail cards, and the live incident feed. The whole page polls on a 15-second tick.
Describe a hypothetical customer request — GPU model, count, commitment length, region preference, compliance requirement — and get back:
- every eligible site, ranked, each with a score breakdown whose segments sum to the reported total;
- every ineligible site with the exact constraint it failed, required-vs-actual, never silently dropped;
- a sensitivity note: the weight at which the ranking would flip, and which site would take first place there.
Allocation Planner — a recommendation, its arithmetic, and what it eliminated.
Monitoring a fleet that never changes proves nothing. Four controls change the operating conditions everything else is computed under, and the whole product — KPIs, charts, capacity bars, site cards, the incident feed, and the ranking — recomputes against the same altered world:
| Control | What it changes |
|---|---|
| Demand spike | Allocates idle capacity fleet-wide. Utilization, power draw, operating cost and workload count all rise; free GPUs fall. |
| Data center outage | Takes a share of one site's accelerators offline and opens an incident. At 15% or more the incident is critical and stage 1 withholds the site; below that it stays eligible and simply scores worse. |
| Electricity price increase | Raises one site's tariff, which lifts its operating cost and weakens its cost-efficiency factor — enough of a rise reorders the ranking. |
| Restore normal operations | Returns to the deterministic baseline, exactly. |
A demand spike, and the KPIs it produces. The banner states the active conditions beside the numbers.
The scenario is an input, not server state. It travels as a compact token in
the URL — ?scenario=demand:60,outage:dc-atx-1:40 — and rides on every request.
Three things follow, and they are the reason it is built this way:
- Concurrent viewers cannot corrupt one another. There is no shared "current scenario" for one browser to change under another's feet, because there isn't one anywhere. The alternative — a server-side session — would reintroduce exactly the mutable global state the simulator is built to avoid, and would make two replicas of the container disagree.
- Determinism survives. The guarantee widens from
f(dc, t, seed)tof(dc, t, seed, scenario)and nothing else changes. The same token at the same timestamp yields the same numbers, in this process or the next. - A scenario is shareable. Pasting a ComputeOps link reproduces the exact world the sender was looking at, down to the byte.
A scenario is a counterfactual world, not an event at a moment. Selecting one re-renders the whole window as if those conditions had always held, so the chart, the KPIs and the planner all describe one coherent world. Modelling it as something that "started just now" would require the metric path to read the wall clock — the one thing the determinism rule forbids, because a point already plotted would then change value later.
Every scenario is still simulated data, and still recommends rather than acts. Nothing is provisioned, reserved, or executed, because there is nothing real behind the numbers to provision.
One deployable service. The React SPA is compiled to static files and served by the same FastAPI process that serves the API, so production is a single container on a single origin — no reverse proxy, no CORS, and no split deployment to keep in step.
flowchart LR
subgraph browser["Browser"]
spa["React SPA<br/><small>Fleet Overview · Allocation Planner</small>"]
url["URL query string<br/><small>?scenario=… · the only place a scenario lives</small>"]
url --- spa
end
subgraph container["Single container · FastAPI on uvicorn"]
direction TB
static["Static files + SPA fallback<br/><small>/ · /allocate survive a hard refresh</small>"]
api["Read-only JSON API<br/><small>/api/*</small>"]
subgraph sim["sim/ — deterministic world"]
world["world.py<br/><small>5 fictional sites, customers, fleets</small>"]
metrics["metrics.py<br/><small>pure f(dc, t, seed, scenario)</small>"]
incidents["incidents.py<br/><small>scheduled from the calendar</small>"]
scen["scenario.py<br/><small>parses the token, projects a world</small>"]
scen --> world
end
subgraph alloc["allocation/ — two-stage engine"]
direction TB
c1["constraints.py<br/><small>stage 1 · eliminate</small>"]
c2["scoring.py<br/><small>stage 2 · rank</small>"]
c3["explain.py<br/><small>rationale + sensitivity</small>"]
c1 --> c2 --> c3
end
policy[("policy/allocation.yaml<br/><small>weights + thresholds</small>")]
end
spa -->|"GET /"| static
spa -->|"GET /api/…?scenario=… · POST /api/allocation/recommend"| api
api --> scen
api --> sim
api --> alloc
alloc --> sim
policy -.->|"re-read on mtime change"| alloc
classDef store fill:#1e4675,stroke:#3987e5,color:#fff;
class policy store;
backend/
computeops/
sim/ world.py (the fictional fleet) · metrics.py (pure f(dc,t,seed,scenario))
incidents.py (deterministic schedule) · workloads.py · clock.py · rng.py
scenario.py (what-if overlays: parsed, projected, never stored)
allocation/ policy.py (loads + validates YAML) · constraints.py (stage 1: eliminate)
scoring.py (stage 2: rank) · explain.py (rationale + sensitivity)
engine.py (orchestration)
api/routes/ fleet · metrics · incidents · allocation
models/ schemas.py — Pydantic, the API contract
static.py serves the built SPA in production; a no-op in development
policy/ allocation.yaml — the audit surface
tests/
frontend/
src/
styles/ tokens.css — one source of truth for color, space, type, radius
api/ types.ts (mirrors the Pydantic schemas) · client.ts · queries.ts
components/ states · badges · KPI tiles · site cards · incidents · charts/
ScenarioControls — the Scenario Control Center
lib/ scenario.ts (token round-trip) · useScenario.ts (URL-backed state)
pages/ FleetOverview · AllocationPlanner
docs/images/ screenshots
Dockerfile · docker-compose.yml · .github/workflows/ci.yml
The HTTP API is read-only with respect to world state. POST /api/allocation/recommend
uses POST only because the request carries a body; it reserves nothing, starts nothing, and
a test asserts that ten identical calls leave the world byte-identical.
Placement runs in two stages, and both stay explainable.
Stage 1 — hard constraints eliminate. Free GPUs of the requested model after holding back a failover reserve, compliance certification, power headroom after the placement, inlet temperature, and open critical incidents. Every rejected site records the constraint it failed with the actual and required numbers, and the UI shows them.
Stage 2 — a weighted sum ranks the survivors. Each factor is normalized 0–100 across the eligible set, multiplied by its weight, and the products are summed. Because the rule is plain arithmetic, every factor's contribution is a real number and the contributions add up to the reported total — the bar in the UI is that sum, not an illustration of it.
| Factor | Weight | Direction |
|---|---|---|
| Cost efficiency — effective $/GPU-hr including energy at the local rate × PUE | 0.30 | lower is better |
| Capacity headroom — GPUs of the requested model free after placement | 0.25 | higher is better |
| Utilization balance — current load; spreads work rather than hot-spotting | 0.15 | lower is better |
| Reliability — severity-weighted availability over the trailing 90 days | 0.15 | higher is better |
| Power margin — facility power budget free after placement | 0.10 | higher is better |
| Region affinity — latency from the customer's preferred region | 0.05 | lower is better |
The weights and thresholds live in
backend/policy/allocation.yaml, not in Python, so the
policy can be reviewed and changed by someone who does not read code. The file is strictly
validated — unknown keys are rejected, factor keys must name something the engine can
actually measure, and weights must be non-negative and sum to 1.0 — because silently
absorbing a typo in an audit surface is worse than failing loudly. It is re-read whenever its
mtime changes, so an edit takes effect on the next request without a restart.
A caveat the UI states plainly: scores are normalized relative to the eligible set, so 100 marks the best of them and 0 the weakest however wide or narrow the real gap is. The measured column always shows the underlying values.
Because the scenario is applied before stage 1 runs, changing conditions changes the decision, not just the numbers beside it:
A 40% outage at Aurora West removes it from the ranking — and says exactly why.
Two worked examples against the same 64 × H100 request, both reproducible from the tokens shown:
| Scenario | Effect on the decision |
|---|---|
outage:dc-atx-1:40 |
Aurora West drops out of the ranking entirely, rejected on two constraints: zero free H100s, and an open critical incident. Four eligible sites become three. |
outage:dc-atx-1:10 |
Below the critical threshold, so Aurora West stays eligible — but its measured availability falls from 99.59% to 89.59%. |
price:dc-atx-1:200 |
Aurora West's tariff doubles and Liffey overtakes it, pushing it out of rank 2. |
price:dc-atx-1:260 |
Aurora West falls further, to the bottom half of the ranking. |
The exact positions depend on the instant sampled, since the fleet is always moving underneath the question — so the test that guards this asserts the direction rather than a fixed order: raising a site's tariff must never improve its rank. The figures above are from the pinned timestamp the test suite uses.
Metrics are a pure function, not accumulated state. f(data_center, timestamp, seed) —
and nothing in that path consults the wall clock. History and the live tick therefore run the
same code, which buys three things: results reproduce across restarts, there is no database,
and the backfilled chart never disagrees with its own ticking right edge. The absence of a
now parameter on those functions is the mechanism, and the test suite asserts it on their
signatures.
Daily series are means over 24 hourly samples, never a single point sample. Point-sampling one instant per day reads each site at a different phase of its own local cycle, which can rank the 30-day view in the opposite order to the 7-day view of the same data.
Randomness is BLAKE2b, not Python's hash(), which is salted per process and would break
reproducibility after every restart. A test runs the simulator under two different
PYTHONHASHSEED values and asserts identical output.
Cost is derived, not invented. Power draw × PUE × the site's local electricity rate, plus amortized hardware — so the cost chart and the allocator's cost factor come from the same relationship rather than two made-up numbers that could disagree.
The five sites have deliberately conflicting strengths, because a fleet where one site wins on every axis gives the allocator nothing interesting to say:
| Site | Region | Strength | Tradeoff |
|---|---|---|---|
| Aurora West (Austin) | us-central | Cheapest power in the fleet | Hot climate, worst PUE, least reliable |
| Cascade (Hillsboro) | us-west | Best PUE, most reliable, hydro power | Almost completely full; thin power margin |
| Potomac (Ashburn) | us-east | Most free capacity, best east-coast latency | Second-most expensive power |
| Liffey (Dublin) | eu-west | Only site with EU data residency | Expensive Irish grid pricing |
| Equator (Singapore) | apac | Newest hardware, excellent uptime | Priciest GPU-hour; tropical heat |
Sensitivity notes are verified, not solved. The weight at which the ranking flips is found by re-running the real ranking across the same 1% grid the UI prints, and the note names whichever site actually takes rank 1 there. Solving it pairwise against the runner-up is wrong: a third site frequently wins at that weight.
Every async surface implements four states — loading (skeletons sized to the real content), error (what failed, plus retry), empty (why, and what to do), populated — and each fails and recovers independently rather than taking the page down. A failed background poll never replaces populated content; it adds a staleness notice and a refresh, because silently serving numbers that stopped updating is the worse failure on an operations dashboard.
Design. Dark-first, because that is how monitoring surfaces are actually read, with light fully specified rather than an afterthought. All color flows through a single token layer. Series and status colors were validated across both modes for lightness band, chroma floor, colorblind separation, and contrast; three light-mode series colors fall below 3:1 against the light surface, which is why the utilization chart ships a table view alongside the legend — that is the required relief, not a nicety. Status is never carried by color alone: every status pairs a hue with a glyph and a text label.
The deployed instance is at computeops.onrender.com —
one free Docker web service built from the Dockerfile below, redeployed on every push to
main. To run it yourself:
Requires Docker 24+.
docker compose up --build
# → http://localhost:8000 the dashboard
# → http://localhost:8000/docs the generated API referenceOr without Compose:
docker build -t computeops:local .
docker run --rm -p 8000:8000 computeops:localThe image is multi-stage: Node builds the frontend, uv resolves backend dependencies from
uv.lock into a virtualenv, and the runtime stage carries only the venv, the application,
the policy file, and the compiled static assets. It runs as an unprivileged user, declares a
health check against /health, and needs no database — the simulation is stateless, so any
replica of the container answers identically.
Requires Python 3.11+ with uv and Node 20.19+ or 22+.
# Backend — http://127.0.0.1:8000 (API docs at /docs)
cd backend
uv sync
uv run uvicorn computeops.main:app --reload
# Frontend — http://localhost:5173 (Vite proxies /api to the backend)
cd frontend
npm install
npm run devIn development the Vite dev server owns the frontend and backend/computeops/static.py
does nothing; it only takes effect when a built dist/ is present, as it is in the image.
curl http://localhost:8000/health
# {"status":"ok","simulated":true,"executes_gpu_jobs":false,"policy_version":"1.2.0"}/health and /api/health are the same check. Both load the policy file, so a container
whose audit surface is missing or malformed reports unhealthy rather than answering
recommendations it cannot justify.
cd backend && uv run pytest # 273 tests
cd frontend && npm run typecheck # tsc --noEmit
cd frontend && npm run lint # oxlint, zero warnings
cd frontend && npm test # 53 tests (vitest)
cd frontend && npm run build # production buildCI (.github/workflows/ci.yml) runs all of the above on every
push and pull request, plus a Docker job that builds the image and smoke-tests the running
container — the API answers, the SPA shell is served, /allocate survives a hard refresh,
and unknown API paths return JSON 404 rather than the HTML shell. Nothing is published from
CI; releasing an image is a separate, deliberate step.
The suite enforces the properties this project rests on:
- Determinism — identical metrics for the same timestamp across processes, hash seeds, and whenever the question is asked; identical rankings for repeated identical requests.
- Explanation integrity — per-factor contributions sum to the reported total for every candidate; every rejected site carries a non-empty failing constraint; every sensitivity note is re-checked by re-running the ranking at the weight it prints; and every published hard constraint matches one the engine can actually emit.
- No execution — an AST audit of the application source asserts zero imports of
subprocess, container/Kubernetes/SSH clients, or cloud SDKs; zero process-launching calls; and zero outbound network clients. Repeated recommendation calls leave world state byte-identical. - Accounting — eligible + rejected always covers the whole fleet; no candidate silently vanishes.
- Single-origin serving — client routes survive a hard refresh, the shell is never cached while fingerprinted assets are cached forever, and paths escaping the static root are refused.
- Scenario integrity — every scenario is deterministic and side-effect free; projecting a world never mutates the shared one; a malformed token is a 422 on every endpoint rather than a silent fall back to baseline; reset restores the baseline byte for byte; and interleaved requests under different scenarios never contaminate each other.
Internal tools. This is the shape of an internal tool: an operations audience that already knows the domain, dense information without hand-holding, four honest states on every async surface, and accessibility treated as a requirement rather than a pass at the end. The hardest parts were not the charts — they were deciding what a stale poll should look like, and making a rejection legible enough to act on.
Operational automation. The allocation engine automates a judgement that is usually manual, without hiding it. The policy is a reviewable file, the arithmetic is a sum a reader can check by hand, and the output includes what was eliminated and what would change the answer. Automation that can be argued with is automation an operations team will actually adopt.
AI infrastructure. The domain model is the real one: GPU pools by model, allocation versus availability versus offline, power budgets and PUE, inlet temperature, incident severity and its effect on measured availability, and cost per GPU-hour derived from energy rather than asserted. The tradeoffs the planner weighs — cheap power against thermal headroom, free capacity against reliability, latency against data residency — are the tradeoffs that make fleet placement a decision instead of a lookup.
Cross-functional decision support. The output is built for a conversation between people who don't share a vocabulary. Sales sees a ranked answer and a latency number; finance sees effective $/GPU-hour; operations sees power margin and inlet temperature; and everyone sees the same score breakdown, from the same weights, in a file any of them can read. The sensitivity note exists for exactly the meeting where someone says "but what if cost mattered more?" — it answers with the weight, and with which site would actually win there.
The Scenario Control Center is the other half of that meeting. "What if Aurora West loses a hall?" and "what if Dublin's power bill doubles?" stop being hypotheticals someone has to model offline: the conditions are set in the UI, and every surface — capacity, cost, incidents, and the ranked recommendation — answers together, from one consistent world, with the assumptions stated on screen.
Stated plainly, because a portfolio project that overclaims is worse than a small one that doesn't:
- The data is simulated. It is internally consistent and deterministic, but it was not measured from anything. None of it is evidence about real hardware, real prices, or real failure rates.
- The policy is illustrative. The weights are a plausible starting point chosen to make the tradeoffs visible, not a calibrated model of anyone's actual placement economics.
- Scores are relative to the eligible set. A 100 means "best of the sites that qualified today", not "good in absolute terms". This is why the measured values are always shown.
- No authentication, no multi-tenancy, no persistence. The API is read-only and the world is stateless, so there is nothing to protect and nothing to store — but that also means none of the access-control work a real internal tool needs is present here.
- Single-process by default. The container runs one uvicorn worker. That is sufficient for a stateless, CPU-light read path, and horizontal scaling is left to the orchestrator.
- The fleet is fixed at five sites. They are defined in code, not configuration, because their deliberately conflicting strengths are part of what the project is demonstrating.
- A scenario rewrites history, not just the present. Because it is a counterfactual world rather than an event, a 40% outage reads as though it has been in effect for the whole 90-day window, which is why measured availability falls so far. That is internally consistent and deterministic, but it is not a simulation of an outage beginning.
- Relative normalization can flatter a damaged site. Taking GPUs offline lowers a site's utilization, and the policy rewards lower utilization, so a small outage can nudge a site's utilization-balance factor up even as its reliability factor falls. The measured column shows both, but the composite score can move less than intuition expects.
- A price increase saturates. Once a site is the worst on cost among the eligible set it scores 0 on that factor, and raising the tariff further cannot lower it. Beyond that point the ranking stops responding to price alone.
MIT © 2026 Yash Shah.