Fix Engine WebUI staging and logging outputs

This commit is contained in:
2025-10-26 02:02:05 -06:00
parent 01ea3ca4a4
commit 1b6d015124
5 changed files with 107 additions and 66 deletions

View File

@@ -198,7 +198,7 @@ function Ensure-EngineWebInterface {
[string]$ProjectRoot
)
$engineSource = Join-Path $ProjectRoot 'Data\Engine\web-interface'
$engineSource = Join-Path $ProjectRoot 'Engine\web-interface'
$legacySource = Join-Path $ProjectRoot 'Data\Server\WebUI'
if (-not (Test-Path $legacySource)) {
@@ -1108,7 +1108,7 @@ switch ($choice) {
$venvFolder = "Server"
$dataSource = "Data"
$dataDestination = "$venvFolder\Borealis"
$customUIPath = "$dataSource\Engine\web-interface"
$customUIPath = (Join-Path $scriptDir 'Engine\web-interface')
$webUIDestination = "$venvFolder\web-interface"
$venvPython = Join-Path $venvFolder 'Scripts\python.exe'
@@ -1408,7 +1408,7 @@ switch ($choice) {
Run-Step "Copy Borealis Engine WebUI Files into: $webUIDestination" {
Ensure-EngineWebInterface -ProjectRoot $scriptDir
$engineWebUISource = Join-Path $engineSourceAbsolute 'web-interface'
$engineWebUISource = Join-Path $scriptDir 'Engine\web-interface'
if (Test-Path $engineWebUISource) {
$webUIDestinationAbsolute = Join-Path $scriptDir $webUIDestination
if (Test-Path $webUIDestinationAbsolute) {

View File

@@ -331,7 +331,7 @@ PY
}
ensure_engine_webui_source() {
local engineSource="Data/Engine/web-interface"
local engineSource="Engine/web-interface"
local legacySource="Data/Server/WebUI"
if [[ -d "${engineSource}/src" && -f "${engineSource}/package.json" ]]; then
return 0
@@ -351,7 +351,7 @@ ensure_engine_webui_source() {
}
prepare_webui() {
local customUIPath="Data/Engine/web-interface"
local customUIPath="Engine/web-interface"
local webUIDestination="Server/web-interface"
ensure_engine_webui_source || return 1
mkdir -p "$webUIDestination"

View File

@@ -112,8 +112,10 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
key_path.write_text("test-key", encoding="utf-8")
bundle_path.write_text(bundle_contents, encoding="utf-8")
log_path = tmp_path / "logs" / "server.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
logs_dir = tmp_path / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
log_path = logs_dir / "server.log"
error_log_path = logs_dir / "error.log"
config = {
"DATABASE_PATH": str(db_path),
@@ -121,6 +123,7 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
"TLS_KEY_PATH": str(key_path),
"TLS_BUNDLE_PATH": str(bundle_path),
"LOG_FILE": str(log_path),
"ERROR_LOG_FILE": str(error_log_path),
"API_GROUPS": ("tokens", "enrollment"),
}

View File

@@ -26,8 +26,9 @@ defaults that mirror the legacy server runtime. Key environment variables are
When TLS values are not provided explicitly the Engine falls back to the
certificate helper shipped with the legacy server, ensuring bundling parity.
Logs are written to ``Logs/Server/server.log`` with daily rotation so the new
runtime integrates with existing operational practices.
Logs are written to ``Logs/Engine/engine.log`` with daily rotation and
errors are additionally duplicated to ``Logs/Engine/error.log`` so the
runtime integrates with the platform's logging policy.
"""
from __future__ import annotations
@@ -48,7 +49,9 @@ except Exception: # pragma: no-cover - Engine configuration still works without
ENGINE_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = ENGINE_DIR.parent.parent
DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db"
LOG_FILE_PATH = PROJECT_ROOT / "Logs" / "Server" / "server.log"
LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine"
LOG_FILE_PATH = LOG_ROOT / "engine.log"
ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log"
def _ensure_parent(path: Path) -> None:
@@ -61,16 +64,27 @@ def _ensure_parent(path: Path) -> None:
def _resolve_static_folder() -> str:
candidates = [
ENGINE_DIR / "web-interface" / "build",
ENGINE_DIR / "web-interface" / "dist",
candidate_roots = [
ENGINE_DIR.parent / "Engine" / "web-interface",
ENGINE_DIR / "web-interface",
]
candidates = []
for root in candidate_roots:
absolute_root = root.resolve()
candidates.extend(
[
absolute_root / "build",
absolute_root / "dist",
absolute_root,
]
)
for candidate in candidates:
absolute = candidate.resolve()
if absolute.is_dir():
return str(absolute)
return str(candidates[0].resolve())
if candidate.is_dir():
return str(candidate)
return str(candidates[0])
def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]:
@@ -139,6 +153,7 @@ class EngineSettings:
tls_key_path: Optional[str]
tls_bundle_path: Optional[str]
log_file: str
error_log_file: str
api_groups: Tuple[str, ...]
raw: MutableMapping[str, Any] = field(default_factory=dict)
@@ -219,6 +234,9 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH)
_ensure_parent(Path(log_file))
error_log_file = str(runtime_config.get("ERROR_LOG_FILE") or ERROR_LOG_FILE_PATH)
_ensure_parent(Path(error_log_file))
api_groups = _parse_api_groups(
runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS")
)
@@ -237,6 +255,7 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
tls_key_path=tls_key_path if tls_key_path else None,
tls_bundle_path=tls_bundle_path if tls_bundle_path else None,
log_file=str(log_file),
error_log_file=str(error_log_file),
api_groups=api_groups,
raw=runtime_config,
)
@@ -244,7 +263,7 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.engine") -> logging.Logger:
"""Configure the Engine logger to write to the shared server log."""
"""Configure the Engine logger to write to Engine log files."""
logger = logging.getLogger(name)
if not logger.handlers:
@@ -263,6 +282,16 @@ def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.eng
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
error_handler = TimedRotatingFileHandler(
settings.error_log_file,
when="midnight",
backupCount=0,
encoding="utf-8",
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
logger.addHandler(error_handler)
logger.setLevel(logging.INFO)
logger.propagate = False
return logger

View File

@@ -4,8 +4,8 @@ Stage 1 introduced the structural skeleton for the Engine runtime. Stage 2
builds upon that foundation by centralising configuration handling and logging
initialisation so the Engine mirrors the legacy server's start-up behaviour.
The factory delegates configuration resolution to :mod:`Data.Engine.config`
and emits structured logs to ``Logs/Server/server.log`` to align with the
project's operational practices.
and emits structured logs to ``Logs/Engine/engine.log`` (with an accompanying
error log) to align with the project's operational practices.
"""
from __future__ import annotations
@@ -16,22 +16,27 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Optional, Sequence, Tuple
import eventlet
try: # pragma: no-cover - optional dependency when running without eventlet
import eventlet # type: ignore
except Exception: # pragma: no-cover - fall back to threading mode
eventlet = None # type: ignore[assignment]
logging.getLogger(__name__).warning(
"Eventlet is not available; Engine will run Socket.IO in threading mode."
)
else: # pragma: no-cover - monkey patch only when eventlet is present
eventlet.monkey_patch(thread=False)
from flask import Flask, request
from flask_cors import CORS
from flask_socketio import SocketIO
from werkzeug.middleware.proxy_fix import ProxyFix
# Eventlet ensures Socket.IO long-polling and WebSocket support parity with the
# legacy server. We keep thread pools enabled for compatibility with blocking
# filesystem/database operations.
eventlet.monkey_patch(thread=False)
try: # pragma: no-cover - defensive import mirroring the legacy runtime.
if eventlet:
try: # pragma: no-cover - defensive import mirroring the legacy runtime.
from eventlet.wsgi import HttpProtocol # type: ignore
except Exception: # pragma: no-cover - the Engine should still operate without it.
except Exception: # pragma: no-cover - the Engine should still operate without it.
HttpProtocol = None # type: ignore[assignment]
else:
else:
_original_handle_one_request = HttpProtocol.handle_one_request
def _quiet_tls_http_mismatch(self): # type: ignore[override]
@@ -73,6 +78,10 @@ else:
return None
HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment]
else:
HttpProtocol = None # type: ignore[assignment]
_SOCKETIO_ASYNC_MODE = "eventlet" if eventlet else "threading"
# Ensure the legacy ``Modules`` package is importable when running from the
@@ -171,7 +180,7 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
socketio = SocketIO(
app,
cors_allowed_origins="*",
async_mode="eventlet",
async_mode=_SOCKETIO_ASYNC_MODE,
engineio_options={
"max_http_buffer_size": 100_000_000,
"max_websocket_message_size": 100_000_000,