feat(cloud-api): bake cloud-console SPA into the image and serve at /console
Multi-stage Dockerfile: stage 1 (node:20-bookworm-slim) builds cloud-console with vite base "/console/"; stage 2 (uv) copies dist/ to /app/console-static. Cloud API mounts the SPA at /console via SpaStaticFiles (StaticFiles subclass that falls back to index.html for deep-link refreshes) when the new CLOUD_CONSOLE_STATIC_DIR env is set, and 307-redirects / to /console/. Static files bypass bearer auth (the SPA shell is public; tokens are still required for /v1/*). Compose enables the mount by default; local dev still uses npm run dev + CLOUD_CONSOLE_CORS_ORIGINS. Jenkinsfile passes mirror overrides (NODE_IMAGE, NPM_REGISTRY, UV_IMAGE, APT_MIRROR, UV_INDEX_URL) as --build-arg, defaulting to CN mirrors (registry.jerryyan.net, registry.npmmirror.com, registry-ghcr.jerryyan.top, mirrors.aliyun.com) so CN builds don't time out; Dockerfile ARGs default to official upstreams so `docker build .` still works anywhere. Backend suite: 443 passed (-m "not integration"); cloud-console typecheck and production build succeed with the new base path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ class CloudControlConfig:
|
||||
credentials: tuple[BearerCredential, ...] = ()
|
||||
enrollment_credentials: tuple[EnrollmentCredential, ...] = ()
|
||||
cors_allowed_origins: tuple[str, ...] = ()
|
||||
console_static_dir: str | None = None
|
||||
|
||||
|
||||
def load_control_config(
|
||||
@@ -94,6 +95,9 @@ def load_control_config(
|
||||
cors_allowed_origins=_parse_cors_origins(
|
||||
values.get("CLOUD_CONSOLE_CORS_ORIGINS")
|
||||
),
|
||||
console_static_dir=_parse_optional_string(
|
||||
values.get("CLOUD_CONSOLE_STATIC_DIR")
|
||||
),
|
||||
)
|
||||
validate_control_config(config)
|
||||
return config
|
||||
@@ -229,3 +233,10 @@ def _parse_cors_origins(raw_value: str | None) -> tuple[str, ...]:
|
||||
if raw_value is None or not raw_value.strip():
|
||||
return ()
|
||||
return tuple(origin.strip() for origin in raw_value.split(",") if origin.strip())
|
||||
|
||||
|
||||
def _parse_optional_string(raw_value: str | None) -> str | None:
|
||||
if raw_value is None:
|
||||
return None
|
||||
stripped = raw_value.strip()
|
||||
return stripped or None
|
||||
|
||||
Reference in New Issue
Block a user