feat(onboarding): add first-run environment checker (#1290)

* feat(onboarding): add first-run environment checker

Add a preflight that checks whether the host can run Docker deploys before a
deploy fails for an avoidable reason. It verifies the Docker engine is reachable
and permitted, the Compose plugin is present, the compose directory is writable
and mounted at a matching host path, the dashboard is behind TLS, and the
compose volume has disk headroom. Each result that needs attention carries a
specific fix rather than a generic error, and the checks never block: an
operator who knows their setup can continue.

The checks run as the final step of first-boot setup and can be re-run any time
from the Recovery settings tab. A new admin-only endpoint,
GET /api/diagnostics/environment, backs both surfaces.

* fix(onboarding): distinguish unverified path mapping and support parent binds

Treat a container whose self-inspect fails as an unverified path-mapping warning
instead of a false "not containerized" pass, so an unverifiable mapping never
reads as healthy. Resolve the compose directory through the longest-prefix bind
mount and compare the host path it resolves to, so a parent bind such as
-v /opt:/opt correctly covers COMPOSE_DIR=/opt/compose instead of warning that
the directory is not bind-mounted.

* test(e2e): advance the setup wizard past the environment step in loginAs

The first-run setup helper clicked "Initialize console" and immediately waited
for the dashboard, but setup now shows an environment-preflight step before
landing the console. Click "Enter Sencho" to complete onboarding before
asserting the dashboard, so the first test on a fresh instance passes.
This commit is contained in:
Anso
2026-06-02 21:40:38 -04:00
committed by GitHub
parent a8f0ce9072
commit 5289f01bfd
11 changed files with 989 additions and 2 deletions
+20
View File
@@ -1,6 +1,7 @@
import { Router, type Request, type Response } from 'express';
import { requireAdmin, requireUserSession } from '../middleware/tierGates';
import { collectDiagnostics } from '../services/DiagnosticsService';
import { collectEnvironmentReport, buildRealProbes } from '../services/EnvironmentCheckService';
import DockerController from '../services/DockerController';
import { withTimeout } from '../utils/withTimeout';
@@ -36,3 +37,22 @@ diagnosticsRouter.get('/', async (req: Request, res: Response): Promise<void> =>
res.status(500).json({ error: 'Failed to collect diagnostics.' });
}
});
// First-run / preflight environment checks (Docker engine + Compose, the
// compose directory and its host path mapping, TLS, disk headroom). Same admin
// session gate as the recovery report. proto / host come from the request so
// the TLS verdict reflects how this browser reached the dashboard; behind a
// reverse proxy that terminates TLS, x-forwarded-proto carries the real scheme.
diagnosticsRouter.get('/environment', async (req: Request, res: Response): Promise<void> => {
if (!requireUserSession(req, res)) return;
if (!requireAdmin(req, res)) return;
try {
const proto = (req.get('x-forwarded-proto')?.split(',')[0].trim()) || req.protocol;
const host = req.get('host') || '';
const report = await collectEnvironmentReport(buildRealProbes({ proto, host }));
res.json(report);
} catch (err) {
console.error('[diagnostics] failed to collect environment report:', (err as Error).message);
res.status(500).json({ error: 'Failed to collect environment checks.' });
}
});