04-May-2026 — make Create Environment wizard work end-to-end closes #258

build 637commit 8fb1097


The Environment Setup wizard at /admin/environment-management/setup did not work on any deployed machine. Five failure modes stacked on top of each other depending on how far a run got. This change addresses all of them so that clicking Create Environment, on a deployed machine, ends up with a fully working Fly app, and clicking Resume on a half-completed environment finishes cleanly with no manual flyctl intervention.

Failure mode 1: persistent non-vcs/fly-io/configs.json read

The wizard ran inside the production Fly machine. non-vcs/ is gitignored and never makes it into the Docker image, so the deploy died on ENOENT and the residual code path then called process.exit(1), taking the whole production server with it. Both wizard entry points (handleEnvironmentCreate and handleEnvironmentSetup) hit the same read at deployToFlyio:141.

What changed

Failure mode 2: errors never reached the browser

readConfigFile and several other helpers called process.exit(1) on failure. From inside an HTTP/WebSocket request handler that killed the production process before the wizard's sendError(ws, ...) could fire. Fly then restarted the machine.

What changed

Failure mode 3: AUTH_SECRET (and other generated secrets) were not persisted

The wizard generated AUTH_SECRET and a full secrets bundle, wrote them to a local file under non-vcs/secrets/ (useless on a deployed machine), and passed them in-memory to deployToFlyio. The first deploy worked because flyctl secrets import ran with that in-memory bundle, but the resume flow loaded secrets from the DB - and the DB record never had AUTH_SECRET or the rest of the bundle written into it.

What changed

Failure mode 4: NODE_ENV-dependent path resolver caused silent SPA 404s

Discovered while testing on a freshly resumed environment. After the AUTH_SECRET pre-flight passed, the deployed Fly app booted but every request to / returned Cannot GET /. Root cause: server/lib/shared/path-utils.ts had a resolveClientPath whose output depended on a runtime check of NODE_ENV === "production". The wizard's buildSecretsConfig always sets NODE_ENV: "production" for new environments, but environments created during the original bug window had an incomplete persisted secrets bundle and the resume flow imported only the keys it could reconstruct, which did not include NODE_ENV. The Fly app booted with NODE_ENV empty, the resolver took the dev branch, the resolved distFolder did not exist, the static-asset middleware was skipped, and Express fell through to default 404.

What changed

Failure mode 5: brevo template migration ENOENT shared the same root cause

The migration server/lib/mongo/migrations/database/20260120000000-update-brevo-transactional-template.ts reads brevo HTML templates via localTemplatePath in server/lib/brevo/templates/local-template-reader.ts, which calls resolveClientPath. On the deployed container during the wizard's initial bootstrap, NODE_ENV was unset, so resolveClientPath returned /usr/src/app/server/ts-gen/projects/ngx-ramblers/src/brevo/templates/fully-automated-text-body.html - a path that does not exist in the image. ENOENT, migration FAILED, wizard's "Migration Failed" page rendered. A retry succeeded only because by then the user was running locally, where the resolver landed somewhere that happened to have the files.

What changed

The failure mode 4 fix is the entire fix here - localTemplatePath now lands at <projectRoot>/projects/ngx-ramblers/src/brevo/templates/<name>.html consistently, regardless of NODE_ENV or which machine the migration runs on. No changes to the migration or the brevo resolver were needed.

Cleanup

How they slipped through

All five failure modes share the same shape: deployment-environment-only bugs that dev laptops never exercise, because dev laptops happen to satisfy the fragile assumptions by accident - they have the local non-vcs/ files, they always run with NODE_ENV set in the shell, and they have the source tree sitting at the resolved paths regardless of how the resolver computes them. Wizard / migration changes need at least one CI run that exercises a clean container - no host filesystem, no inherited shell env - before being declared green.

Verification

End-to-end behaviour

Clicking Create Environment in the wizard, on a deployed machine, ends up with a fully working Fly app: the env record and full secrets bundle (including NODE_ENV) are written to the DB, dockerImage is read from the DB, the deploy runs with secrets passed in-memory and imported to Fly, the env entry is upserted with the deploy token at the end, the SPA serves /, and migrations that read bundled assets resolve them from the project root regardless of NODE_ENV state. Clicking Resume on a half-completed env reads the full secrets bundle back from the DB and produces a working app without any manual flyctl secrets set. A simulated failure inside deployToFlyio (bad Fly API token, missing required secret) produces a red error in the wizard UI with the failure details, and the production server stays up.

Out of scope