Files
roa2web-service-auto/deployment/windows/scripts/Build-Frontend.ps1
Marius Mutu 6b13ffa183 Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
2025-10-25 14:55:08 +03:00

586 lines
19 KiB
PowerShell

<#
.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"
}
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 '<transferred-path>'"
}
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