diff --git a/Borealis.ps1 b/Borealis.ps1 index f921b818..ab6c34e6 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -270,6 +270,41 @@ function Ensure-EngineWebInterface { } } +function Get-WebUiLatestWriteTime { + param([string]$Root) + + if (-not (Test-Path $Root)) { return $null } + + $exclusions = @('\node_modules\', '\build\', '\dist\') + $files = Get-ChildItem -Path $Root -Recurse -File -ErrorAction SilentlyContinue | Where-Object { + $full = $_.FullName + -not ($exclusions | Where-Object { $full -like "*$_*" }) + } + if (-not $files) { return $null } + return ($files | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1).LastWriteTime +} + +function Test-WebUiBuildFresh { + param( + [string]$SourceRoot, + [string]$BuildRoot + ) + + $sourceLatest = Get-WebUiLatestWriteTime -Root $SourceRoot + if (-not $sourceLatest) { return $false } + + $buildIndex = Join-Path $BuildRoot 'index.html' + if (-not (Test-Path $buildIndex -PathType Leaf)) { return $false } + + try { + $buildTime = (Get-Item $buildIndex -ErrorAction Stop).LastWriteTime + } catch { + return $false + } + + return ($buildTime -ge $sourceLatest) +} + $script:Utf8CodePageChanged = $false function Ensure-SystemUtf8CodePage { @@ -1184,6 +1219,16 @@ switch ($choice) { } } + if ($engineImmediateLaunch) { + $webUiSourceRoot = Join-Path $scriptDir 'Data\Engine\web-interface' + $webUiBuildRoot = Join-Path $scriptDir 'Engine\web-interface\build' + $webUiFresh = Test-WebUiBuildFresh -SourceRoot $webUiSourceRoot -BuildRoot $webUiBuildRoot + if (-not $webUiFresh) { + Write-Host "Detected WebUI changes newer than the last production build. Running full build instead of Quick/Skip." -ForegroundColor Yellow + $engineImmediateLaunch = $false + } + } + if ($engineModeChoice -notin @('1','2','3')) { break } diff --git a/Data/Engine/bootstrapper.py b/Data/Engine/bootstrapper.py index 5d4bc2a1..08d008f9 100644 --- a/Data/Engine/bootstrapper.py +++ b/Data/Engine/bootstrapper.py @@ -88,6 +88,25 @@ def _stage_web_interface_assets(logger: Optional[logging.Logger] = None, *, forc engine_web_root = project_root / "Engine" / "web-interface" modern_source = project_root / "Data" / "Engine" / "web-interface" + def _latest_mtime(root: Path) -> Optional[float]: + """Return the most recent mtime under root, excluding build artifacts.""" + + try: + candidates = [] + for path in root.rglob("*"): + # Skip heavy/ephemeral directories so we only compare source files + if any(part in {"node_modules", "build", "dist"} for part in path.parts): + continue + try: + stat = path.stat() + except OSError: + continue + if path.is_file(): + candidates.append(stat.st_mtime) + return max(candidates) if candidates else None + except Exception: + return None + if not modern_source.is_dir(): raise RuntimeError( f"Engine web interface source missing: {modern_source}" @@ -95,8 +114,15 @@ def _stage_web_interface_assets(logger: Optional[logging.Logger] = None, *, forc 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 + source_mtime = _latest_mtime(modern_source) + dest_mtime = _latest_mtime(engine_web_root) + if dest_mtime is not None and source_mtime is not None and dest_mtime >= source_mtime: + logger.info("Engine web interface already staged at %s; skipping copy (destination up-to-date).", engine_web_root) + return engine_web_root + logger.info( + "Engine web interface detected newer source assets; refreshing staging directory %s.", + engine_web_root, + ) if engine_web_root.exists(): shutil.rmtree(engine_web_root) diff --git a/Data/Engine/web-interface/src/App.jsx b/Data/Engine/web-interface/src/App.jsx index bbd8c701..3a0bf18e 100644 --- a/Data/Engine/web-interface/src/App.jsx +++ b/Data/Engine/web-interface/src/App.jsx @@ -1092,10 +1092,6 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state"; [navigateTo, setTabs, setActiveTabId] ); - useEffect(() => { - setPageHeader({ title: "", subtitle: "", Icon: null }); - }, [currentPage]); - const isAdmin = (String(userRole || '').toLowerCase() === 'admin'); useEffect(() => {