mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 09:21:57 -06:00
Fixed Engine Flask Server Accessibility
This commit is contained in:
30
.vscode/tasks.json
vendored
30
.vscode/tasks.json
vendored
@@ -31,6 +31,36 @@
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Borealis - Engine (Production)",
|
||||
"type": "shell",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", "${workspaceFolder}/Borealis.ps1",
|
||||
"-EngineProduction"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Borealis - Engine (Dev)",
|
||||
"type": "shell",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-ExecutionPolicy", "Bypass",
|
||||
"-File", "${workspaceFolder}/Borealis.ps1",
|
||||
"-EngineDev"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Borealis - Agent - Deploy",
|
||||
"type": "shell",
|
||||
|
||||
48
Borealis.ps1
48
Borealis.ps1
@@ -10,6 +10,8 @@ param(
|
||||
[switch]$Flask,
|
||||
[switch]$Quick,
|
||||
[switch]$EngineTests,
|
||||
[switch]$EngineProduction,
|
||||
[switch]$EngineDev,
|
||||
[string]$InstallerCode = ''
|
||||
)
|
||||
|
||||
@@ -17,6 +19,7 @@ param(
|
||||
$choice = $null
|
||||
$modeChoice = $null
|
||||
$agentSubChoice = $null
|
||||
$engineModeChoice = $null
|
||||
|
||||
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||
|
||||
@@ -48,6 +51,16 @@ if ($Vite -and $Flask) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($EngineProduction -and $EngineDev) {
|
||||
Write-Host "Cannot combine -EngineProduction and -EngineDev." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (($EngineProduction -or $EngineDev) -and ($Server -or $Agent)) {
|
||||
Write-Host "Engine automation switches cannot be combined with -Server or -Agent." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Server) {
|
||||
# Auto-select main menu option for Server when -Server flag is provided
|
||||
$choice = '1'
|
||||
@@ -60,6 +73,10 @@ if ($Server) {
|
||||
'launch' { $agentSubChoice = '4' }
|
||||
default { $agentSubChoice = '1' }
|
||||
}
|
||||
} elseif ($EngineProduction -or $EngineDev) {
|
||||
$choice = '5'
|
||||
if ($EngineProduction) { $engineModeChoice = '1' }
|
||||
if ($EngineDev) { $engineModeChoice = '3' }
|
||||
}
|
||||
|
||||
if ($Server) {
|
||||
@@ -1078,7 +1095,11 @@ switch ($choice) {
|
||||
foreach ($tool in @($pythonExe, $nodeExe, $npmCmd, $npxCmd)) {
|
||||
if (-not (Test-Path $tool)) { Write-Host "`r$($symbols.Fail) Bundled executable not found at '$tool'." -ForegroundColor Red; exit 1 }
|
||||
}
|
||||
$env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), (Split-Path $nodeExe), $env:PATH
|
||||
$nodeDir = Split-Path $nodeExe
|
||||
$env:BOREALIS_NODE_DIR = $nodeDir
|
||||
$env:BOREALIS_NPM_CMD = $npmCmd
|
||||
$env:BOREALIS_NPX_CMD = $npxCmd
|
||||
$env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), $nodeDir, $env:PATH
|
||||
|
||||
if (-not $modeChoice) {
|
||||
Write-Host " "
|
||||
@@ -1315,14 +1336,22 @@ switch ($choice) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
$env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), (Split-Path $nodeExe), $env:PATH
|
||||
$nodeDir = Split-Path $nodeExe
|
||||
$env:BOREALIS_NODE_DIR = $nodeDir
|
||||
$env:BOREALIS_NPM_CMD = $npmCmd
|
||||
$env:BOREALIS_NPX_CMD = $npxCmd
|
||||
$env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), $nodeDir, $env:PATH
|
||||
|
||||
if (-not $engineModeChoice) {
|
||||
Write-Host " "
|
||||
Write-Host "Configure Borealis Engine Mode:" -ForegroundColor DarkYellow
|
||||
Write-Host " 1) Build & Launch > Production Flask Server @ http://localhost:5001" -ForegroundColor DarkCyan
|
||||
Write-Host " 2) [Skip Build] & Immediately Launch > Production Flask Server @ http://localhost:5001" -ForegroundColor DarkCyan
|
||||
Write-Host " 1) Build & Launch > Production Flask Server @ https://localhost:5000" -ForegroundColor DarkCyan
|
||||
Write-Host " 2) [Skip Build] & Immediately Launch > Production Flask Server @ https://localhost:5000" -ForegroundColor DarkCyan
|
||||
Write-Host " 3) Launch > [Hotload-Ready] Vite Dev Server @ http://localhost:5173" -ForegroundColor DarkCyan
|
||||
$engineModeChoice = Read-Host "Enter choice [1/2/3]"
|
||||
} else {
|
||||
Write-Host "Auto-selecting Borealis Engine mode option $engineModeChoice." -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
$engineOperationMode = "production"
|
||||
$engineImmediateLaunch = $false
|
||||
@@ -1347,7 +1376,7 @@ switch ($choice) {
|
||||
$previousEngineMode = $env:BOREALIS_ENGINE_MODE
|
||||
$previousEnginePort = $env:BOREALIS_ENGINE_PORT
|
||||
$env:BOREALIS_ENGINE_MODE = $engineOperationMode
|
||||
$env:BOREALIS_ENGINE_PORT = "5001"
|
||||
$env:BOREALIS_ENGINE_PORT = "5000"
|
||||
Write-Host "`nLaunching Borealis Engine..." -ForegroundColor Green
|
||||
Write-Host "===================================================================================="
|
||||
Write-Host "$($symbols.Running) Engine Socket Server Started..."
|
||||
@@ -1448,10 +1477,13 @@ switch ($choice) {
|
||||
$webUIDestinationAbsolute = Join-Path $scriptDir $webUIDestination
|
||||
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)
|
||||
if ($engineOperationMode -eq "developer") {
|
||||
Start-Process -NoNewWindow -FilePath $npmCmd -ArgumentList @("run", "dev")
|
||||
} else {
|
||||
& $npmCmd run build
|
||||
}
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
@@ -1462,7 +1494,7 @@ switch ($choice) {
|
||||
$previousEngineMode = $env:BOREALIS_ENGINE_MODE
|
||||
$previousEnginePort = $env:BOREALIS_ENGINE_PORT
|
||||
$env:BOREALIS_ENGINE_MODE = $engineOperationMode
|
||||
$env:BOREALIS_ENGINE_PORT = "5001"
|
||||
$env:BOREALIS_ENGINE_PORT = "5000"
|
||||
Write-Host "`nLaunching Borealis Engine..." -ForegroundColor Green
|
||||
Write-Host "===================================================================================="
|
||||
Write-Host "$($symbols.Running) Engine Socket Server Started..."
|
||||
|
||||
@@ -32,11 +32,11 @@ Lastly, everytime that you complete a stage, you will create a pull request name
|
||||
- [x] Replace legacy API routes with Engine-provided blueprints gated by a flag.
|
||||
- [x] Emit transitional logging when Engine handles requests.
|
||||
- [ ] **Stage 6 — Plan WebUI migration**
|
||||
- [ ] Move static/template handling into Data/Engine/services/WebUI.
|
||||
- [x] Move static/template handling into Data/Engine/services/WebUI.
|
||||
- [x] Ensure that data from /Data/Server/WebUI is copied into /Engine/web-interface during engine Deployment via Borealis.ps1
|
||||
- [ ] Preserve TLS-aware URL generation and caching.
|
||||
- [x] Preserve TLS-aware URL generation and caching.
|
||||
- [ ] Add migration switch in the legacy server for WebUI delegation.
|
||||
- [ ] Extend tests to cover critical WebUI routes.
|
||||
- [x] Extend tests to cover critical WebUI routes.
|
||||
- [ ] **Stage 7 — Plan WebSocket migration**
|
||||
- [ ] Extract Socket.IO handlers into Data/Engine/services/WebSocket.
|
||||
- [ ] Provide register_realtime hook for the Engine factory.
|
||||
@@ -44,5 +44,5 @@ Lastly, everytime that you complete a stage, you will create a pull request name
|
||||
- [ ] Update legacy server to consume Engine WebSocket registration.
|
||||
|
||||
## Current Status
|
||||
- **Stage:** Stage 5 — Bridge the legacy server to Engine APIs (completed)
|
||||
- **Active Task:** Awaiting next stage instructions.
|
||||
- **Stage:** Stage 6 — Plan WebUI migration
|
||||
- **Active Task:** Prepare legacy WebUI delegation switch (pending approval to touch legacy server).
|
||||
|
||||
@@ -117,6 +117,13 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
|
||||
log_path = logs_dir / "server.log"
|
||||
error_log_path = logs_dir / "error.log"
|
||||
|
||||
static_dir = tmp_path / "static"
|
||||
static_dir.mkdir(parents=True, exist_ok=True)
|
||||
(static_dir / "index.html").write_text("<html><body>Engine Test UI</body></html>", encoding="utf-8")
|
||||
assets_dir = static_dir / "assets"
|
||||
assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
(assets_dir / "example.txt").write_text("asset", encoding="utf-8")
|
||||
|
||||
config = {
|
||||
"DATABASE_PATH": str(db_path),
|
||||
"TLS_CERT_PATH": str(cert_path),
|
||||
@@ -124,7 +131,8 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
|
||||
"TLS_BUNDLE_PATH": str(bundle_path),
|
||||
"LOG_FILE": str(log_path),
|
||||
"ERROR_LOG_FILE": str(error_log_path),
|
||||
"API_GROUPS": ("tokens", "enrollment"),
|
||||
"STATIC_FOLDER": str(static_dir),
|
||||
"API_GROUPS": ("core", "tokens", "enrollment"),
|
||||
}
|
||||
|
||||
app, _socketio, _context = create_app(config)
|
||||
|
||||
9
Data/Engine/Unit_Tests/test_core_api.py
Normal file
9
Data/Engine/Unit_Tests/test_core_api.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_health_endpoint(engine_harness):
|
||||
client = engine_harness.app.test_client()
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
payload = response.get_json()
|
||||
assert payload == {"status": "ok"}
|
||||
23
Data/Engine/Unit_Tests/test_web_ui.py
Normal file
23
Data/Engine/Unit_Tests/test_web_ui.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def test_web_ui_root_serves_index(engine_harness):
|
||||
client = engine_harness.app.test_client()
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
body = response.get_data(as_text=True)
|
||||
assert "Engine Test UI" in body
|
||||
|
||||
|
||||
def test_web_ui_serves_static_assets(engine_harness):
|
||||
client = engine_harness.app.test_client()
|
||||
response = client.get("/assets/example.txt")
|
||||
assert response.status_code == 200
|
||||
assert response.get_data(as_text=True) == "asset"
|
||||
|
||||
|
||||
def test_web_ui_spa_fallback(engine_harness):
|
||||
client = engine_harness.app.test_client()
|
||||
response = client.get("/devices")
|
||||
assert response.status_code == 200
|
||||
assert "Engine Test UI" in response.get_data(as_text=True)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The bootstrapper assembles configuration via :func:`Data.Engine.config.load_runtime_config`
|
||||
before delegating to :func:`Data.Engine.server.create_app`. It mirrors the
|
||||
legacy server defaults by binding to ``0.0.0.0:5001`` and honouring the
|
||||
legacy server defaults by binding to ``0.0.0.0:5000`` and honouring the
|
||||
``BOREALIS_ENGINE_*`` environment overrides for bind host/port.
|
||||
"""
|
||||
|
||||
@@ -11,14 +11,16 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .server import EngineContext, create_app
|
||||
|
||||
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_PORT = 5001
|
||||
DEFAULT_PORT = 5000
|
||||
|
||||
|
||||
def _project_root() -> Path:
|
||||
@@ -37,16 +39,34 @@ def _project_root() -> Path:
|
||||
|
||||
|
||||
def _build_runtime_config() -> Dict[str, Any]:
|
||||
api_groups_override = os.environ.get("BOREALIS_ENGINE_API_GROUPS")
|
||||
if api_groups_override:
|
||||
api_groups: Any = api_groups_override
|
||||
else:
|
||||
api_groups = ("core", "tokens", "enrollment")
|
||||
|
||||
return {
|
||||
"HOST": os.environ.get("BOREALIS_ENGINE_HOST", DEFAULT_HOST),
|
||||
"PORT": int(os.environ.get("BOREALIS_ENGINE_PORT", DEFAULT_PORT)),
|
||||
"TLS_CERT_PATH": os.environ.get("BOREALIS_TLS_CERT"),
|
||||
"TLS_KEY_PATH": os.environ.get("BOREALIS_TLS_KEY"),
|
||||
"TLS_BUNDLE_PATH": os.environ.get("BOREALIS_TLS_BUNDLE"),
|
||||
"API_GROUPS": api_groups,
|
||||
}
|
||||
|
||||
|
||||
def _stage_web_interface_assets(logger: logging.Logger) -> None:
|
||||
def _stage_web_interface_assets(logger: Optional[logging.Logger] = None, *, force: bool = False) -> Path:
|
||||
"""Ensure Engine web interface assets are staged and return the staging root."""
|
||||
|
||||
if logger is None:
|
||||
logger = logging.getLogger("borealis.engine.bootstrap")
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s-%(name)s-%(levelname)s: %(message)s"))
|
||||
logger.addHandler(handler)
|
||||
logger.propagate = False
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
project_root = _project_root()
|
||||
engine_web_root = project_root / "Engine" / "web-interface"
|
||||
legacy_source = project_root / "Data" / "Server" / "WebUI"
|
||||
@@ -56,20 +76,114 @@ def _stage_web_interface_assets(logger: logging.Logger) -> None:
|
||||
f"Engine web interface source missing: {legacy_source}"
|
||||
)
|
||||
|
||||
index_path = engine_web_root / "index.html"
|
||||
if engine_web_root.exists() and index_path.is_file() and not force:
|
||||
logger.info("Engine web interface already staged at %s; skipping copy.", engine_web_root)
|
||||
return engine_web_root
|
||||
|
||||
if engine_web_root.exists():
|
||||
shutil.rmtree(engine_web_root)
|
||||
|
||||
shutil.copytree(legacy_source, engine_web_root)
|
||||
|
||||
index_path = engine_web_root / "index.html"
|
||||
if not index_path.is_file():
|
||||
raise RuntimeError(
|
||||
f"Engine web interface staging failed; missing {index_path}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Engine web interface staged from %s to %s", legacy_source, engine_web_root
|
||||
logger.info("Engine web interface staged from %s to %s", legacy_source, engine_web_root)
|
||||
return engine_web_root
|
||||
|
||||
|
||||
def _determine_static_folder(staging_root: Path) -> str:
|
||||
for candidate in (staging_root / "build", staging_root / "dist", staging_root):
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
return str(staging_root)
|
||||
|
||||
|
||||
def _resolve_npm_executable() -> str:
|
||||
env_cmd = os.environ.get("BOREALIS_NPM_CMD")
|
||||
if env_cmd:
|
||||
candidate = Path(env_cmd).expanduser()
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
|
||||
node_dir = os.environ.get("BOREALIS_NODE_DIR")
|
||||
if node_dir:
|
||||
candidate = Path(node_dir) / "npm.cmd"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
candidate = Path(node_dir) / "npm.exe"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
candidate = Path(node_dir) / "npm"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
|
||||
if os.name == "nt":
|
||||
return "npm.cmd"
|
||||
return "npm"
|
||||
|
||||
|
||||
def _run_npm(args: list[str], cwd: Path, logger: logging.Logger) -> None:
|
||||
command = [_resolve_npm_executable()] + args
|
||||
logger.info("Running npm command: %s", " ".join(command))
|
||||
start = time.time()
|
||||
try:
|
||||
completed = subprocess.run(command, cwd=str(cwd), capture_output=True, text=True, check=False)
|
||||
except FileNotFoundError as exc:
|
||||
raise RuntimeError("npm executable not found; ensure Node.js dependencies are installed.") from exc
|
||||
duration = time.time() - start
|
||||
if completed.returncode != 0:
|
||||
logger.error(
|
||||
"npm command failed (%ss): %s\nstdout: %s\nstderr: %s",
|
||||
f"{duration:.2f}",
|
||||
" ".join(command),
|
||||
completed.stdout.strip(),
|
||||
completed.stderr.strip(),
|
||||
)
|
||||
raise RuntimeError(f"npm command {' '.join(command)} failed with exit code {completed.returncode}")
|
||||
stdout = completed.stdout.strip()
|
||||
if stdout:
|
||||
logger.debug("npm stdout: %s", stdout)
|
||||
stderr = completed.stderr.strip()
|
||||
if stderr:
|
||||
logger.debug("npm stderr: %s", stderr)
|
||||
logger.info("npm command completed in %.2fs", duration)
|
||||
|
||||
|
||||
def _ensure_web_ui_build(staging_root: Path, logger: logging.Logger, *, mode: str) -> str:
|
||||
package_json = staging_root / "package.json"
|
||||
node_modules = staging_root / "node_modules"
|
||||
build_dir = staging_root / "build"
|
||||
needs_install = not node_modules.is_dir()
|
||||
needs_build = not build_dir.is_dir()
|
||||
|
||||
if not needs_build and package_json.is_file():
|
||||
build_index = build_dir / "index.html"
|
||||
if build_index.is_file():
|
||||
needs_build = build_index.stat().st_mtime < package_json.stat().st_mtime
|
||||
else:
|
||||
needs_build = True
|
||||
|
||||
if mode == "developer":
|
||||
logger.info("Engine launched in developer mode; skipping WebUI build step.")
|
||||
return _determine_static_folder(staging_root)
|
||||
|
||||
if not package_json.is_file():
|
||||
logger.warning("WebUI package.json not found at %s; continuing without rebuild.", package_json)
|
||||
return _determine_static_folder(staging_root)
|
||||
|
||||
if needs_install:
|
||||
_run_npm(["install", "--silent", "--no-fund", "--audit=false"], staging_root, logger)
|
||||
|
||||
if needs_build:
|
||||
_run_npm(["run", "build"], staging_root, logger)
|
||||
else:
|
||||
logger.info("Existing WebUI build found at %s; reuse.", build_dir)
|
||||
|
||||
return _determine_static_folder(staging_root)
|
||||
|
||||
|
||||
def _ensure_tls_material(context: EngineContext) -> None:
|
||||
@@ -137,13 +251,24 @@ def _prepare_tls_run_kwargs(context: EngineContext) -> Dict[str, Any]:
|
||||
|
||||
def main() -> None:
|
||||
config = _build_runtime_config()
|
||||
mode = os.environ.get("BOREALIS_ENGINE_MODE", "production").strip().lower() or "production"
|
||||
try:
|
||||
staging_root = _stage_web_interface_assets()
|
||||
except Exception as exc:
|
||||
logging.getLogger("borealis.engine.bootstrap").error(
|
||||
"Failed to stage Engine web interface: %s", exc
|
||||
)
|
||||
raise
|
||||
|
||||
if staging_root:
|
||||
bootstrap_logger = logging.getLogger("borealis.engine.bootstrap")
|
||||
static_folder = _ensure_web_ui_build(staging_root, bootstrap_logger, mode=mode)
|
||||
config.setdefault("STATIC_FOLDER", static_folder)
|
||||
|
||||
app, socketio, context = create_app(config)
|
||||
|
||||
try:
|
||||
_stage_web_interface_assets(context.logger)
|
||||
except Exception as exc:
|
||||
context.logger.error("Failed to stage Engine web interface: %s", exc)
|
||||
raise
|
||||
if staging_root:
|
||||
context.logger.info("Engine WebUI assets ready at %s", config["STATIC_FOLDER"])
|
||||
|
||||
host = config.get("HOST", DEFAULT_HOST)
|
||||
port = int(config.get("PORT", DEFAULT_PORT))
|
||||
|
||||
@@ -47,7 +47,26 @@ except Exception: # pragma: no-cover - Engine configuration still works without
|
||||
|
||||
|
||||
ENGINE_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = ENGINE_DIR.parent.parent
|
||||
|
||||
|
||||
def _discover_project_root() -> Path:
|
||||
"""Locate the project root by searching for Borealis.ps1 or using overrides."""
|
||||
|
||||
env_override = os.environ.get("BOREALIS_PROJECT_ROOT")
|
||||
if env_override:
|
||||
env_path = Path(env_override).expanduser().resolve()
|
||||
if env_path.is_dir():
|
||||
return env_path
|
||||
|
||||
current = ENGINE_DIR
|
||||
for candidate in (current, *current.parents):
|
||||
if (candidate / "Borealis.ps1").is_file():
|
||||
return candidate
|
||||
|
||||
return ENGINE_DIR.parent.parent
|
||||
|
||||
|
||||
PROJECT_ROOT = _discover_project_root()
|
||||
DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db"
|
||||
LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine"
|
||||
LOG_FILE_PATH = LOG_ROOT / "engine.log"
|
||||
@@ -66,26 +85,28 @@ def _ensure_parent(path: Path) -> None:
|
||||
def _resolve_static_folder() -> str:
|
||||
candidate_roots = [
|
||||
PROJECT_ROOT / "Engine" / "web-interface",
|
||||
PROJECT_ROOT / "web-interface",
|
||||
ENGINE_DIR.parent / "Engine" / "web-interface",
|
||||
ENGINE_DIR.parent / "web-interface",
|
||||
ENGINE_DIR / "web-interface",
|
||||
PROJECT_ROOT / "Data" / "Server" / "web-interface",
|
||||
]
|
||||
|
||||
candidates = []
|
||||
resolved_roots: List[Path] = []
|
||||
for root in candidate_roots:
|
||||
absolute_root = root.resolve()
|
||||
candidates.extend(
|
||||
[
|
||||
absolute_root / "build",
|
||||
absolute_root / "dist",
|
||||
absolute_root,
|
||||
]
|
||||
)
|
||||
absolute_root = root.expanduser().resolve()
|
||||
if absolute_root not in resolved_roots:
|
||||
resolved_roots.append(absolute_root)
|
||||
|
||||
for candidate in candidates:
|
||||
for root in resolved_roots:
|
||||
for candidate in (root / "build", root / "dist", root):
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
|
||||
return str(candidates[0])
|
||||
if resolved_roots:
|
||||
return str(resolved_roots[0])
|
||||
|
||||
return str((PROJECT_ROOT / "Engine" / "web-interface").resolve())
|
||||
|
||||
|
||||
def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]:
|
||||
|
||||
@@ -17,7 +17,7 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, Mapping, Optional, Sequence
|
||||
|
||||
from flask import Flask
|
||||
from flask import Blueprint, Flask, jsonify
|
||||
|
||||
from Modules.auth import jwt_service as jwt_service_module
|
||||
from Modules.auth.dpop import DPoPValidator
|
||||
@@ -191,18 +191,36 @@ _GROUP_REGISTRARS: Mapping[str, Callable[[Flask, LegacyServiceAdapters], None]]
|
||||
}
|
||||
|
||||
|
||||
def _register_core(app: Flask, context: EngineContext) -> None:
|
||||
"""Register core utility endpoints that do not require legacy adapters."""
|
||||
|
||||
blueprint = Blueprint("engine_core", __name__)
|
||||
|
||||
@blueprint.route("/health", methods=["GET"])
|
||||
def health() -> Any:
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
context.logger.info("Engine registered API group 'core'.")
|
||||
|
||||
|
||||
def register_api(app: Flask, context: EngineContext) -> None:
|
||||
"""Register Engine API blueprints based on the enabled groups."""
|
||||
|
||||
enabled_groups: Iterable[str] = context.api_groups or DEFAULT_API_GROUPS
|
||||
normalized = [group.strip().lower() for group in enabled_groups if group]
|
||||
adapters = LegacyServiceAdapters(context)
|
||||
adapters: Optional[LegacyServiceAdapters] = None
|
||||
|
||||
for group in normalized:
|
||||
if group == "core":
|
||||
_register_core(app, context)
|
||||
continue
|
||||
|
||||
if adapters is None:
|
||||
adapters = LegacyServiceAdapters(context)
|
||||
registrar = _GROUP_REGISTRARS.get(group)
|
||||
if registrar is None:
|
||||
context.logger.info("Engine API group '%s' is not implemented; skipping.", group)
|
||||
continue
|
||||
registrar(app, adapters)
|
||||
context.logger.info("Engine registered API group '%s'.", group)
|
||||
|
||||
|
||||
@@ -1,17 +1,86 @@
|
||||
"""WebUI service stubs for the Borealis Engine runtime.
|
||||
|
||||
The future WebUI migration will centralise static asset serving, template
|
||||
rendering, and dev-server proxying here. Stage 1 keeps the placeholder so the
|
||||
application factory can stub out registration calls.
|
||||
"""
|
||||
"""WebUI static asset handling for the Borealis Engine runtime."""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Flask
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from flask import Blueprint, Flask, request, send_from_directory
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from ...server import EngineContext
|
||||
|
||||
_WEBUI_BLUEPRINT_NAME = "engine_webui"
|
||||
|
||||
|
||||
def _resolve_static_root(app: Flask, context: EngineContext) -> Optional[Path]:
|
||||
static_folder = app.static_folder
|
||||
if not static_folder:
|
||||
context.logger.error("Engine WebUI static folder is not configured.")
|
||||
return None
|
||||
|
||||
static_path = Path(static_folder).resolve()
|
||||
if not static_path.is_dir():
|
||||
context.logger.error("Engine WebUI static folder missing: %s", static_path)
|
||||
return None
|
||||
|
||||
index_path = static_path / "index.html"
|
||||
if not index_path.is_file():
|
||||
context.logger.error("Engine WebUI missing index.html at %s", index_path)
|
||||
return None
|
||||
|
||||
return static_path
|
||||
|
||||
|
||||
def _register_spa_routes(app: Flask, static_root: Path, logger: logging.Logger) -> None:
|
||||
blueprint = Blueprint(
|
||||
_WEBUI_BLUEPRINT_NAME,
|
||||
__name__,
|
||||
static_folder=str(static_root),
|
||||
static_url_path="",
|
||||
)
|
||||
|
||||
def send_index():
|
||||
return send_from_directory(str(static_root), "index.html")
|
||||
|
||||
@blueprint.route("/", defaults={"requested_path": ""})
|
||||
@blueprint.route("/<path:requested_path>")
|
||||
def spa_entry(requested_path: str) -> object:
|
||||
if requested_path:
|
||||
try:
|
||||
return send_from_directory(str(static_root), requested_path)
|
||||
except NotFound:
|
||||
logger.debug("Engine WebUI asset not found: %s", requested_path)
|
||||
return send_index()
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
|
||||
if getattr(app, "_engine_webui_fallback_installed", False):
|
||||
return
|
||||
|
||||
def _spa_fallback(error):
|
||||
request_path = (request.path or "").strip()
|
||||
if request_path.startswith("/api") or request_path.startswith("/socket.io"):
|
||||
return error
|
||||
if "." in Path(request_path).name:
|
||||
return error
|
||||
if request.method not in {"GET", "HEAD"}:
|
||||
return error
|
||||
try:
|
||||
return send_index()
|
||||
except Exception:
|
||||
return error
|
||||
|
||||
app.register_error_handler(404, _spa_fallback)
|
||||
setattr(app, "_engine_webui_fallback_installed", True)
|
||||
|
||||
|
||||
def register_web_ui(app: Flask, context: EngineContext) -> None:
|
||||
"""Placeholder hook for WebUI route registration."""
|
||||
"""Register static asset routes for the Engine WebUI."""
|
||||
|
||||
context.logger.debug("Engine WebUI services are not yet implemented.")
|
||||
static_root = _resolve_static_root(app, context)
|
||||
if static_root is None:
|
||||
return
|
||||
|
||||
_register_spa_routes(app, static_root, context.logger)
|
||||
context.logger.info("Engine WebUI registered static assets from %s", static_root)
|
||||
|
||||
Reference in New Issue
Block a user