From 4bac45afe5f3a15dbfa635dca8d40d0a122fd202 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sat, 25 Oct 2025 05:11:43 -0600 Subject: [PATCH] Stage Engine web UI from legacy assets --- Borealis.ps1 | 74 +++++++++++++++++++- Borealis.sh | 39 ++++++++++- Data/Engine/README.md | 8 ++- Data/Engine/config/environment.py | 10 ++- Data/Engine/tests/test_config_environment.py | 31 +++++--- Data/Engine/web-interface/.gitignore | 3 + Data/Engine/web-interface/README.md | 10 +++ Data/Server/server.py | 4 +- 8 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 Data/Engine/web-interface/.gitignore create mode 100644 Data/Engine/web-interface/README.md diff --git a/Borealis.ps1 b/Borealis.ps1 index e911197..a5d7cbf 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -144,6 +144,67 @@ function Write-AgentLog { "[$ts] $Message" | Out-File -FilePath $path -Append -Encoding UTF8 } +function Ensure-EngineTlsMaterial { + param( + [string]$PythonPath, + [string]$CertificateRoot + ) + + if (-not (Test-Path $CertificateRoot)) { + New-Item -Path $CertificateRoot -ItemType Directory -Force | Out-Null + } + + if (Test-Path $PythonPath) { + $code = @' +from Data.Engine.services.crypto import certificates +certificates.ensure_certificate() +'@ + try { + & $PythonPath -c $code | Out-Null + } catch { + Write-Host "Failed to pre-generate Engine TLS certificates: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + $env:BOREALIS_CERT_DIR = $CertificateRoot + $env:BOREALIS_TLS_CERT = Join-Path $CertificateRoot 'borealis-server-cert.pem' + $env:BOREALIS_TLS_KEY = Join-Path $CertificateRoot 'borealis-server-key.pem' + $env:BOREALIS_TLS_BUNDLE = Join-Path $CertificateRoot 'borealis-server-bundle.pem' +} + +function Ensure-EngineWebInterface { + param( + [string]$ProjectRoot + ) + + $engineSource = Join-Path $ProjectRoot 'Data\Engine\web-interface' + $legacySource = Join-Path $ProjectRoot 'Data\Server\WebUI' + + if (-not (Test-Path $legacySource)) { + return + } + + $enginePackage = Join-Path $engineSource 'package.json' + $engineSrcDir = Join-Path $engineSource 'src' + if ((Test-Path $enginePackage) -and (Test-Path $engineSrcDir)) { + return + } + + if (-not (Test-Path $engineSource)) { + New-Item -Path $engineSource -ItemType Directory -Force | Out-Null + } + + $preserve = @('.gitignore','README.md') + Get-ChildItem -Path $engineSource -Force | Where-Object { $preserve -notcontains $_.Name } | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + + Copy-Item (Join-Path $legacySource '*') $engineSource -Recurse -Force + + if (-not (Test-Path $enginePackage)) { + throw "Failed to stage Engine web interface into '$engineSource'." + } +} + $script:Utf8CodePageChanged = $false function Ensure-SystemUtf8CodePage { @@ -1026,7 +1087,7 @@ switch ($choice) { $venvFolder = "Server" $dataSource = "Data" $dataDestination = "$venvFolder\Borealis" - $customUIPath = "$dataSource\Server\WebUI" + $customUIPath = "$dataSource\Engine\web-interface" $webUIDestination = "$venvFolder\web-interface" $venvPython = Join-Path $venvFolder 'Scripts\python.exe' @@ -1076,12 +1137,14 @@ switch ($choice) { } Run-Step "Copy Borealis WebUI Files into: $webUIDestination" { + Ensure-EngineWebInterface -ProjectRoot $scriptDir if (Test-Path $webUIDestination) { Remove-Item "$webUIDestination\public\*" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "$webUIDestination\src\*" -Recurse -Force -ErrorAction SilentlyContinue } else { New-Item -Path $webUIDestination -ItemType Directory -Force | Out-Null } + if (-not (Test-Path $customUIPath)) { throw "Web interface source '$customUIPath' not found." } Copy-Item "$customUIPath\*" $webUIDestination -Recurse -Force } @@ -1095,6 +1158,8 @@ switch ($choice) { Run-Step "Vite Web Frontend: Start ($borealis_operation_mode)" { Push-Location $webUIDestination if ($borealis_operation_mode -eq "developer") { $viteSubCommand = "dev" } else { $viteSubCommand = "build" } + $certRoot = Join-Path $scriptDir 'Certificates\Server' + Ensure-EngineTlsMaterial -PythonPath $venvPython -CertificateRoot $certRoot Start-Process -NoNewWindow -FilePath $npmCmd -ArgumentList @("run",$viteSubCommand) Pop-Location } @@ -1273,7 +1338,7 @@ switch ($choice) { $dataSource = "Data" $engineSource = "$dataSource\Engine" $engineDataDestination = "$venvFolder\Data\Engine" - $webUIFallbackSource = "$dataSource\Server\WebUI" + $webUIFallbackSource = "$dataSource\Server\web-interface" $webUIDestination = "$venvFolder\web-interface" $venvPython = Join-Path $venvFolder 'Scripts\python.exe' $engineSourceAbsolute = Join-Path $scriptDir $engineSource @@ -1315,7 +1380,8 @@ switch ($choice) { } Run-Step "Copy Borealis Engine WebUI Files into: $webUIDestination" { - $engineWebUISource = Join-Path $engineSourceAbsolute 'WebUI' + Ensure-EngineWebInterface -ProjectRoot $scriptDir + $engineWebUISource = Join-Path $engineSourceAbsolute 'web-interface' if (Test-Path $engineWebUISource) { $webUIDestinationAbsolute = Join-Path $scriptDir $webUIDestination if (Test-Path $webUIDestinationAbsolute) { @@ -1356,6 +1422,8 @@ switch ($choice) { if (Test-Path $webUIDestinationAbsolute) { Push-Location $webUIDestinationAbsolute if ($engineOperationMode -eq "developer") { $viteSubCommand = "dev" } else { $viteSubCommand = "build" } + $certRoot = Join-Path $scriptDir 'Certificates\Server' + Ensure-EngineTlsMaterial -PythonPath $venvPython -CertificateRoot $certRoot Start-Process -NoNewWindow -FilePath $npmCmd -ArgumentList @("run", $viteSubCommand) Pop-Location } diff --git a/Borealis.sh b/Borealis.sh index 97e1f1c..ac87508 100644 --- a/Borealis.sh +++ b/Borealis.sh @@ -296,9 +296,45 @@ copy_server_payload() { fi } +ensure_engine_tls_material() { + local python_bin="${SCRIPT_DIR}/Server/bin/python3" + local cert_dir="${SCRIPT_DIR}/Certificates/Server" + mkdir -p "$cert_dir" + if [[ -x "$python_bin" ]]; then + "$python_bin" - <<'PY' +from Data.Engine.services.crypto import certificates +certificates.ensure_certificate() +PY + fi + export BOREALIS_CERT_DIR="$cert_dir" + export BOREALIS_TLS_CERT="${cert_dir}/borealis-server-cert.pem" + export BOREALIS_TLS_KEY="${cert_dir}/borealis-server-key.pem" +} + +ensure_engine_webui_source() { + local engineSource="Data/Engine/web-interface" + local legacySource="Data/Server/WebUI" + if [[ -d "${engineSource}/src" && -f "${engineSource}/package.json" ]]; then + return 0 + fi + if [[ ! -d "$legacySource" ]]; then + echo "${RED}Legacy WebUI source '$legacySource' not found.${RESET}" >&2 + return 1 + fi + mkdir -p "$engineSource" + find "$engineSource" -mindepth 1 -maxdepth 1 \ + ! -name '.gitignore' ! -name 'README.md' -exec rm -rf {} + + cp -a "$legacySource/." "$engineSource/" + if [[ ! -f "${engineSource}/package.json" ]]; then + echo "${RED}Failed to stage Engine web interface into '$engineSource'.${RESET}" >&2 + return 1 + fi +} + prepare_webui() { - local customUIPath="Data/Server/WebUI" + local customUIPath="Data/Engine/web-interface" local webUIDestination="Server/web-interface" + ensure_engine_webui_source || return 1 mkdir -p "$webUIDestination" rm -rf "${webUIDestination}/public" "${webUIDestination}/src" "${webUIDestination}/build" 2>/dev/null || true cp -r "${customUIPath}/"* "$webUIDestination/" @@ -311,6 +347,7 @@ vite_start() { local webUIDestination="Server/web-interface" local subcmd="build"; [[ "$mode" == "developer" ]] && subcmd="dev" ensure_node_bins + ensure_engine_tls_material ( cd "$webUIDestination" && nohup "$NPM_BIN" run "$subcmd" >/dev/null 2>&1 & ) } diff --git a/Data/Engine/README.md b/Data/Engine/README.md index b83cb85..060e3c0 100644 --- a/Data/Engine/README.md +++ b/Data/Engine/README.md @@ -42,7 +42,7 @@ The Engine mirrors the legacy defaults so it can boot without additional configu | `BOREALIS_ROOT` | Overrides automatic project root detection. Useful when running from a packaged location. | Directory two levels above `Data/Engine/` | | `BOREALIS_DATABASE_PATH` | Path to the SQLite database. | `/database.db` | | `BOREALIS_ENGINE_AUTO_MIGRATE` | Run Engine-managed schema migrations during bootstrap (`true`/`false`). | `true` | -| `BOREALIS_STATIC_ROOT` | Directory that serves static assets for the SPA. | First existing path among `Engine/web-interface/build`, `Engine/web-interface/dist`, `Data/Engine/WebUI/build`, `Data/Server/web-interface/build`, `Data/Server/WebUI/build`, `Data/WebUI/build` | +| `BOREALIS_STATIC_ROOT` | Directory that serves static assets for the SPA. | First existing path among `Engine/web-interface/build`, `Engine/web-interface/dist`, `Data/Engine/web-interface/build`, `Data/Server/WebUI/build`, `Data/Server/web-interface/build`, `Data/WebUI/build` | | `BOREALIS_CORS_ALLOWED_ORIGINS` | Comma-delimited list of origins granted CORS access. Use `*` for all origins. | `*` | | `BOREALIS_FLASK_SECRET_KEY` | Secret key for Flask session signing. | `change-me` | | `BOREALIS_DEBUG` | Enables debug logging, disables secure-cookie requirements, and allows Werkzeug debug mode. | `false` | @@ -55,6 +55,12 @@ The Engine mirrors the legacy defaults so it can boot without additional configu | `BOREALIS_CERTIFICATES_ROOT` | Overrides where TLS certificates (root CA + leaf) are stored. | `/Certificates` | | `BOREALIS_SERVER_CERT_ROOT` | Directly points to the Engine server certificate directory if certificates are staged elsewhere. | `/Certificates/Server` | +The launch scripts (`Borealis.ps1` / `Borealis.sh`) automatically synchronize +`Data/Server/WebUI` into `Data/Engine/web-interface` when the Engine’s copy is +missing. The repository keeps that directory mostly empty (except for +documentation) so Git history does not duplicate the large SPA payload, but the +runtime staging still ensures Vite reads from the Engine tree. + ## TLS and transport stack `Data/Engine/services/crypto/certificates.py` mirrors the legacy certificate diff --git a/Data/Engine/config/environment.py b/Data/Engine/config/environment.py index 04211f8..8186d36 100644 --- a/Data/Engine/config/environment.py +++ b/Data/Engine/config/environment.py @@ -120,16 +120,14 @@ def _resolve_static_root(project_root: Path) -> Path: project_root / "Engine" / "web-interface" / "build", project_root / "Engine" / "web-interface" / "dist", project_root / "Engine" / "web-interface", - project_root / "Data" / "Engine" / "WebUI" / "build", - project_root / "Data" / "Engine" / "WebUI", + project_root / "Data" / "Engine" / "web-interface" / "build", + project_root / "Data" / "Engine" / "web-interface", project_root / "Server" / "web-interface" / "build", project_root / "Server" / "web-interface", - project_root / "Server" / "WebUI" / "build", - project_root / "Server" / "WebUI", - project_root / "Data" / "Server" / "web-interface" / "build", - project_root / "Data" / "Server" / "web-interface", project_root / "Data" / "Server" / "WebUI" / "build", project_root / "Data" / "Server" / "WebUI", + project_root / "Data" / "Server" / "web-interface" / "build", + project_root / "Data" / "Server" / "web-interface", project_root / "Data" / "WebUI" / "build", project_root / "Data" / "WebUI", ) diff --git a/Data/Engine/tests/test_config_environment.py b/Data/Engine/tests/test_config_environment.py index 7631925..d3e687c 100644 --- a/Data/Engine/tests/test_config_environment.py +++ b/Data/Engine/tests/test_config_environment.py @@ -46,27 +46,27 @@ def test_static_root_env_override(tmp_path, monkeypatch): monkeypatch.delenv("BOREALIS_ROOT", raising=False) -def test_static_root_falls_back_to_legacy_source(tmp_path, monkeypatch): - """Legacy WebUI source should be served when no build assets exist.""" +def test_static_root_falls_back_to_engine_source(tmp_path, monkeypatch): + """Engine data assets should serve when no build output exists.""" - legacy_source = tmp_path / "Data" / "Server" / "WebUI" - legacy_source.mkdir(parents=True) - (legacy_source / "index.html").write_text("", encoding="utf-8") + engine_source = tmp_path / "Data" / "Engine" / "web-interface" + engine_source.mkdir(parents=True) + (engine_source / "index.html").write_text("", encoding="utf-8") monkeypatch.setenv("BOREALIS_ROOT", str(tmp_path)) monkeypatch.delenv("BOREALIS_STATIC_ROOT", raising=False) settings = load_environment() - assert settings.flask.static_root == legacy_source.resolve() + assert settings.flask.static_root == engine_source.resolve() monkeypatch.delenv("BOREALIS_ROOT", raising=False) def test_static_root_considers_runtime_copy(tmp_path, monkeypatch): - """Runtime Server/WebUI copies should be considered when Data assets are missing.""" + """Runtime Server/web-interface copies should be considered when Data assets are missing.""" - runtime_source = tmp_path / "Server" / "WebUI" + runtime_source = tmp_path / "Server" / "web-interface" runtime_source.mkdir(parents=True) (runtime_source / "index.html").write_text("runtime", encoding="utf-8") @@ -76,7 +76,22 @@ def test_static_root_considers_runtime_copy(tmp_path, monkeypatch): settings = load_environment() assert settings.flask.static_root == runtime_source.resolve() + monkeypatch.delenv("BOREALIS_ROOT", raising=False) + +def test_static_root_falls_back_to_legacy_assets(tmp_path, monkeypatch): + """Legacy Data/Server/WebUI assets remain a valid fallback.""" + + legacy_source = tmp_path / "Data" / "Server" / "WebUI" + legacy_source.mkdir(parents=True) + (legacy_source / "index.html").write_text("legacy", encoding="utf-8") + + monkeypatch.setenv("BOREALIS_ROOT", str(tmp_path)) + monkeypatch.delenv("BOREALIS_STATIC_ROOT", raising=False) + + settings = load_environment() + + assert settings.flask.static_root == legacy_source.resolve() monkeypatch.delenv("BOREALIS_ROOT", raising=False) diff --git a/Data/Engine/web-interface/.gitignore b/Data/Engine/web-interface/.gitignore new file mode 100644 index 0000000..7c9d611 --- /dev/null +++ b/Data/Engine/web-interface/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!README.md diff --git a/Data/Engine/web-interface/README.md b/Data/Engine/web-interface/README.md new file mode 100644 index 0000000..f8e5472 --- /dev/null +++ b/Data/Engine/web-interface/README.md @@ -0,0 +1,10 @@ +# Engine Web Interface Staging + +The Engine server reuses the existing Vite single-page application that still lives under +`Data/Server/WebUI`. At runtime the launch scripts copy those assets into this directory so the +Engine can stage its own copy without disturbing the legacy server. + +The repository intentionally ignores the staged files to avoid duplicating tens of thousands of +lines (and large binary assets) in source control. If you need to refresh the Engine copy, run one +of the launch scripts (`Borealis.ps1` or `Borealis.sh`) or copy the assets manually from +`Data/Server/WebUI` into this folder. diff --git a/Data/Server/server.py b/Data/Server/server.py index c22baa7..07dcd40 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -657,8 +657,8 @@ from job_scheduler import set_credential_fetcher as scheduler_set_credential_fet _STATIC_CANDIDATES = [ os.path.join(os.path.dirname(__file__), '../web-interface/build'), - os.path.join(os.path.dirname(__file__), 'WebUI/build'), - os.path.join(os.path.dirname(__file__), '../WebUI/build'), + os.path.join(os.path.dirname(__file__), '../web-interface/dist'), + os.path.join(os.path.dirname(__file__), '../web-interface'), ] _resolved_static_folder = None