<# .SYNOPSIS Build ROA2WEB Frontend for Production Deployment .DESCRIPTION This script builds the Vue.js frontend for Windows Server deployment: - Checks for Node.js installation - Installs npm dependencies - Builds production-optimized static files - Creates deployment package with backend files - Optionally transfers to remote server .PARAMETER BackendSource Path to backend source (default: ../../reports-app/backend) .PARAMETER FrontendSource Path to frontend source (default: ../../reports-app/frontend) .PARAMETER OutputPath Output path for deployment package (default: ./deploy-package) .PARAMETER ServerPath Remote server path for automatic deployment (optional) .PARAMETER ServerHost Remote server hostname/IP for automatic deployment (optional) .EXAMPLE .\Build-Frontend.ps1 Build with defaults, output to ./deploy-package .EXAMPLE .\Build-Frontend.ps1 -OutputPath "D:\deployments\roa2web-$(Get-Date -Format 'yyyyMMdd')" Build to custom output path .EXAMPLE .\Build-Frontend.ps1 -ServerHost "10.0.20.170" -ServerPath "C:\Temp\roa2web-deploy" Build and transfer to remote server .NOTES Author: ROA2WEB Team Requires: Node.js 16+, npm Can run on: WSL, Windows, Linux #> [CmdletBinding()] param( [string]$BackendSource = "../../reports-app/backend", [string]$FrontendSource = "../../reports-app/frontend", [string]$OutputPath = "./deploy-package", [string]$ServerHost = "", [string]$ServerPath = "" ) $ErrorActionPreference = "Stop" # ============================================================================= # HELPER FUNCTIONS # ============================================================================= function Write-Step { param([string]$Message) Write-Host "`n[*] $Message" -ForegroundColor Cyan } function Write-Success { param([string]$Message) Write-Host " [OK] $Message" -ForegroundColor Green } function Write-Error { param([string]$Message) Write-Host " [ERROR] $Message" -ForegroundColor Red } function Write-Warning { param([string]$Message) Write-Host " [WARN] $Message" -ForegroundColor Yellow } function Test-NodeJS { Write-Step "Checking Node.js installation..." try { $nodeVersion = node --version 2>&1 $npmVersion = npm --version 2>&1 Write-Success "Node.js: $nodeVersion" Write-Success "npm: $npmVersion" # Check minimum version (16.x) if ($nodeVersion -match "v(\d+)\.") { $major = [int]$matches[1] if ($major -lt 16) { throw "Node.js version 16+ required (found: $nodeVersion)" } } return $true } catch { Write-Error "Node.js not found or version too old" Write-Host "`n Install Node.js from: https://nodejs.org/" -ForegroundColor Yellow Write-Host " Minimum version: 16.x" -ForegroundColor Yellow throw } } function Resolve-FullPath { param([string]$Path) $scriptDir = Split-Path -Parent $PSScriptRoot $fullPath = Join-Path $scriptDir $Path # Convert to absolute path and resolve .. and . properly $fullPath = [System.IO.Path]::GetFullPath($fullPath) return $fullPath } function Build-Frontend { param([string]$SourcePath) Write-Step "Building Vue.js frontend..." if (-not (Test-Path $SourcePath)) { throw "Frontend source path not found: $SourcePath" } Push-Location $SourcePath try { # Clean node_modules if it exists (to avoid EPERM errors) $nodeModulesPath = Join-Path $SourcePath "node_modules" if (Test-Path $nodeModulesPath) { Write-Step "Cleaning existing node_modules..." try { Remove-Item -Path $nodeModulesPath -Recurse -Force -ErrorAction Stop Write-Success "Cleaned node_modules" } catch { Write-Warning "Could not remove node_modules: $_" Write-Warning "Please close VS Code/IDE and try again, or run as Administrator" throw "Cannot proceed with locked node_modules. Close all IDEs and retry." } } # Install dependencies Write-Step "Installing npm dependencies (this may take a minute)..." # Show npm output for transparency, redirect to host to prevent capture npm install | Out-Default # Verify node_modules was created if (-not (Test-Path $nodeModulesPath)) { throw "npm install failed: node_modules not created. Check errors above." } Write-Success "Dependencies installed" # Build for production Write-Step "Building for production (this may take a minute)..." $env:NODE_ENV = "production" # Show build output for transparency, redirect to host to prevent capture npm run build | Out-Default Write-Success "Build completed" # Verify dist folder $distPath = Join-Path $SourcePath "dist" if (-not (Test-Path $distPath)) { throw "Build failed: dist folder not found" } $distFiles = Get-ChildItem -Path $distPath -Recurse -File $totalSize = ($distFiles | Measure-Object -Property Length -Sum).Sum / 1MB Write-Success "Generated $(($distFiles).Count) files (Total: $([math]::Round($totalSize, 2)) MB)" return $distPath } finally { Pop-Location } } function Copy-BackendFiles { param( [string]$SourcePath, [string]$DestPath ) Write-Step "Copying backend files..." if (-not (Test-Path $SourcePath)) { throw "Backend source path not found: $SourcePath" } # Ensure destination exists if (-not (Test-Path $DestPath)) { New-Item -ItemType Directory -Path $DestPath -Force | Out-Null } # Exclude directory names (will skip entire directory trees) $excludeDirs = @( "venv", "__pycache__", ".pytest_cache", "logs", "temp", "node_modules" ) # Exclude file patterns $excludeFiles = @( "*.pyc", "*.pyo", "*.log", ".env", ".env.local" ) # Normalize source path (ensure trailing backslash for proper substring calculation) $normalizedSourcePath = $SourcePath.TrimEnd('\', '/') + '\' # Helper function to check if path should be excluded (using script: scope to access parent variables) $testExclude = { param([string]$RelativePath, [bool]$IsDirectory, [array]$ExcludeDirs, [array]$ExcludeFiles) # Check directory exclusions (match directory name exactly) if ($IsDirectory) { $dirName = Split-Path $RelativePath -Leaf if ($ExcludeDirs -contains $dirName) { return $true } } # Check if any parent directory should be excluded $pathParts = $RelativePath -split '[\\/]' foreach ($part in $pathParts) { if ($ExcludeDirs -contains $part) { return $true } } # Check file patterns if (-not $IsDirectory) { foreach ($pattern in $ExcludeFiles) { if ($RelativePath -like $pattern) { return $true } } } return $false } # Copy files Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object { # Calculate relative path safely if ($_.FullName.Length -le $normalizedSourcePath.Length) { return # Skip if path is too short (shouldn't happen, but safety check) } $relativePath = $_.FullName.Substring($normalizedSourcePath.Length) # Check if should be excluded $shouldExclude = & $testExclude -RelativePath $relativePath -IsDirectory $_.PSIsContainer -ExcludeDirs $excludeDirs -ExcludeFiles $excludeFiles if ($shouldExclude) { return } $destFile = Join-Path $DestPath $relativePath if ($_.PSIsContainer) { if (-not (Test-Path $destFile)) { New-Item -ItemType Directory -Path $destFile -Force | Out-Null } } else { $destDir = Split-Path $destFile -Parent if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } Copy-Item -Path $_.FullName -Destination $destFile -Force } } $backendFiles = Get-ChildItem -Path $DestPath -Recurse -File Write-Success "Copied $(($backendFiles).Count) backend files" # Verify critical files are present $requirementsTxt = Join-Path $DestPath "requirements.txt" if (-not (Test-Path $requirementsTxt)) { Write-Error "CRITICAL: requirements.txt not found in backend package!" Write-Host " Expected: $requirementsTxt" -ForegroundColor Red throw "Backend package incomplete - missing requirements.txt" } Write-Success "Verified: requirements.txt present" } function New-DeploymentPackage { param( [string]$FrontendDistPath, [string]$BackendSourcePath, [string]$OutputPath ) Write-Step "Creating deployment package..." # Create output directory if (Test-Path $OutputPath) { Write-Warning "Output path exists, cleaning..." Remove-Item -Path $OutputPath -Recurse -Force } New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null # Create structure $frontendDest = Join-Path $OutputPath "frontend" $backendDest = Join-Path $OutputPath "backend" New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null New-Item -ItemType Directory -Path $backendDest -Force | Out-Null # Copy frontend dist Write-Step "Copying frontend files..." Copy-Item -Path "$FrontendDistPath\*" -Destination $frontendDest -Recurse -Force Write-Success "Frontend files copied" # Copy backend files Copy-BackendFiles -SourcePath $BackendSourcePath -DestPath $backendDest # Copy shared modules (database, auth, utils) Write-Step "Copying shared modules..." $sharedSource = Join-Path (Split-Path (Split-Path $BackendSourcePath -Parent) -Parent) "shared" $sharedDest = Join-Path $OutputPath "shared" if (Test-Path $sharedSource) { Copy-Item -Path $sharedSource -Destination $sharedDest -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests") Write-Success "Shared modules copied" } else { Write-Warning "Shared modules not found at: $sharedSource" } # Copy deployment config $configSource = Join-Path (Split-Path -Parent $PSScriptRoot) "config" $configDest = Join-Path $OutputPath "config" if (Test-Path $configSource) { Copy-Item -Path $configSource -Destination $configDest -Recurse -Force Write-Success "Config files copied" } # Copy deployment scripts Write-Step "Copying deployment scripts..." $scriptsSource = $PSScriptRoot $scriptsDest = Join-Path $OutputPath "scripts" New-Item -ItemType Directory -Path $scriptsDest -Force | Out-Null # List of scripts to include in deployment package $deploymentScripts = @( "Install-ROA2WEB.ps1", "Deploy-ROA2WEB.ps1", "Start-ROA2WEB.ps1", "Stop-ROA2WEB.ps1", "Restart-ROA2WEB.ps1" ) $copiedScripts = 0 foreach ($script in $deploymentScripts) { $scriptPath = Join-Path $scriptsSource $script if (Test-Path $scriptPath) { Copy-Item -Path $scriptPath -Destination $scriptsDest -Force $copiedScripts++ } } Write-Success "Copied $copiedScripts deployment scripts" # Create README $readmePath = Join-Path $OutputPath "README.txt" $readme = @" ================================================================================ ROA2WEB DEPLOYMENT PACKAGE Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss") ================================================================================ CONTENTS: --------- backend/ FastAPI backend application files (Python) frontend/ Vue.js static files (built for production) config/ IIS configuration files (.env template, web.config) scripts/ PowerShell management scripts DEPLOYMENT SCRIPTS: ------------------- Install-ROA2WEB.ps1 First-time setup (Python venv, IIS site) Deploy-ROA2WEB.ps1 Update application files (auto-detects source) Start-ROA2WEB.ps1 Start backend service + IIS Stop-ROA2WEB.ps1 Stop backend service + IIS Restart-ROA2WEB.ps1 Quick restart ================================================================================ DEPLOYMENT WORKFLOW ================================================================================ >> FIRST TIME INSTALLATION: --------------------------- 1. Copy this entire folder to server (e.g., C:\Deploy\ROA2WEB-v1) 2. Open PowerShell as Administrator: cd C:\Deploy\ROA2WEB-v1\scripts .\Install-ROA2WEB.ps1 3. Configure environment: notepad C:\inetpub\wwwroot\roa2web\backend\.env 4. Start services: .\Start-ROA2WEB.ps1 5. Access: http://localhost:8080 >> UPDATES (New code version): ------------------------------- 1. Copy new deployment package to server (e.g., C:\Deploy\ROA2WEB-v2) 2. Open PowerShell as Administrator: cd C:\Deploy\ROA2WEB-v2\scripts 3. Deploy (automatically stops, updates, and starts): .\Stop-ROA2WEB.ps1 .\Deploy-ROA2WEB.ps1 .\Start-ROA2WEB.ps1 Note: Deploy-ROA2WEB.ps1 auto-detects source path (no parameters needed!) 4. Done! Application updated with new code. >> QUICK OPERATIONS: -------------------- Restart app: .\Restart-ROA2WEB.ps1 Stop app: .\Stop-ROA2WEB.ps1 Start app: .\Start-ROA2WEB.ps1 ================================================================================ REQUIREMENTS ================================================================================ - Windows Server 2016+ or Windows 10/11 - IIS already installed (with ASP.NET Core Hosting Bundle) - Python 3.8+ installed - PowerShell 5.1+ (run as Administrator) NOTES: ------ • Backend files do NOT include venv (virtual environment) • Install-ROA2WEB.ps1 creates venv and installs dependencies automatically • Deploy-ROA2WEB.ps1 creates backup before updating • .env files are preserved during updates • Application installs to: C:\inetpub\wwwroot\roa2web\ TROUBLESHOOTING: ---------------- Check logs: C:\inetpub\wwwroot\roa2web\logs\ Backend logs: C:\inetpub\wwwroot\roa2web\backend\backend.log IIS logs: C:\inetpub\logs\LogFiles\ For detailed documentation, see: WINDOWS_DEPLOYMENT.md ================================================================================ "@ Set-Content -Path $readmePath -Value $readme -Force # Calculate package size $packageFiles = Get-ChildItem -Path $OutputPath -Recurse -File $packageSize = ($packageFiles | Measure-Object -Property Length -Sum).Sum / 1MB Write-Success "Deployment package created: $OutputPath" Write-Success "Total files: $(($packageFiles).Count)" Write-Success "Total size: $([math]::Round($packageSize, 2)) MB" return $OutputPath } function Copy-ToRemoteServer { param( [string]$LocalPath, [string]$ServerHost, [string]$ServerPath ) Write-Step "Transferring to remote server..." try { # Check if remote server is accessible $pingResult = Test-Connection -ComputerName $ServerHost -Count 1 -Quiet if (-not $pingResult) { Write-Warning "Server $ServerHost not reachable" return $false } # Use robocopy for efficient transfer (Windows) if ($IsWindows -or $env:OS -match "Windows") { $remotePath = "\\$ServerHost\$($ServerPath -replace ':', '$')" Write-Host " [*] Copying to: $remotePath" -ForegroundColor Yellow robocopy $LocalPath $remotePath /E /Z /R:3 /W:5 /MT:8 /NFL /NDL /NP if ($LASTEXITCODE -le 7) { Write-Success "Files transferred successfully" return $true } else { Write-Error "Transfer failed with code: $LASTEXITCODE" return $false } } else { # Use scp for Unix/WSL Write-Warning "Remote copy via SCP not yet implemented" Write-Host " Manual transfer required to: $ServerHost`:$ServerPath" -ForegroundColor Yellow return $false } } catch { Write-Error "Failed to transfer to server: $_" return $false } } # ============================================================================= # MAIN BUILD FLOW # ============================================================================= function Main { Write-Host @" ==================================================================== ROA2WEB - Frontend Build Script Build Vue.js frontend and create deployment package ==================================================================== "@ -ForegroundColor Cyan try { # Resolve paths $backendSourcePath = Resolve-FullPath -Path $BackendSource $frontendSourcePath = Resolve-FullPath -Path $FrontendSource $outputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Resolve-FullPath -Path $OutputPath } Write-Host "`nPaths:" -ForegroundColor Yellow Write-Host " Backend Source: $backendSourcePath" Write-Host " Frontend Source: $frontendSourcePath" Write-Host " Output Path: $outputPath" # Check Node.js Test-NodeJS # Build frontend $distPath = Build-Frontend -SourcePath $frontendSourcePath # Create deployment package $packagePath = New-DeploymentPackage ` -FrontendDistPath $distPath ` -BackendSourcePath $backendSourcePath ` -OutputPath $outputPath # Transfer to server if specified if ($ServerHost -and $ServerPath) { $transferred = Copy-ToRemoteServer ` -LocalPath $packagePath ` -ServerHost $ServerHost ` -ServerPath $ServerPath } Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host " BUILD COMPLETED SUCCESSFULLY" -ForegroundColor Green Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host "`nDeployment Package: $packagePath" -ForegroundColor Yellow if ($ServerHost) { Write-Host "`nNext Steps (on server $ServerHost):" -ForegroundColor Yellow Write-Host " cd $ServerPath" Write-Host " .\Deploy-ROA2WEB.ps1 -SourcePath ." } else { Write-Host "`nNext Steps:" -ForegroundColor Yellow Write-Host " 1. Transfer '$packagePath' to your Windows Server" Write-Host " 2. On the server, run: Deploy-ROA2WEB.ps1 -SourcePath ''" } Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan } catch { Write-Host "`n[FATAL ERROR] Build failed: $_" -ForegroundColor Red Write-Host $_.ScriptStackTrace -ForegroundColor Red exit 1 } } # Run main build Main