Fixes 3 critical issues preventing production deployment on Windows IIS: 1. **IIS Sub-Application Path Stripping** - Changed URL patterns from ^roa2web/api/(.*) to ^api/(.*) - IIS sub-app at /roa2web automatically strips prefix - Requests arrive as /api/* not /roa2web/api/* 2. **SPA Fallback Absolute Path** - Changed from url="/index.html" to url="index.html" - Absolute paths (/) refer to site root, not sub-app - Relative path correctly serves from sub-app 3. **MIME Type Duplicates (500 Error)** - Added <remove> before <mimeMap> for .js, .json, .webmanifest - Prevents "duplicate collection entry" errors - Allows override of server-level MIME types Build Script Improvements: - Build-ROA2WEB.ps1: Copy public/ folder to temp build dir - Build-ROA2WEB.ps1: Added verification logging for web.config - ROA2WEB-Console.ps1: Fixed web.config verification location Cleanup: - Removed outdated web.config.10.0.20.36-INTERNAL - Removed temporary test files and docs Tested: https://roa2web.romfast.ro/roa2web/ - login page loads successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1088 lines
38 KiB
PowerShell
1088 lines
38 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Unified Build Script for ROA2WEB Windows Deployment
|
|
|
|
.DESCRIPTION
|
|
This script builds complete deployment packages for ROA2WEB application.
|
|
Supports building Frontend, Backend, Telegram Bot, or all components together.
|
|
|
|
Features:
|
|
- Flexible component selection
|
|
- Validates dependencies (Node.js, Python)
|
|
- Creates production-optimized builds
|
|
- Generates deployment-ready packages
|
|
- Supports automatic server transfer
|
|
|
|
.PARAMETER Component
|
|
Component(s) to build (optional - shows interactive menu if not specified):
|
|
- All: Build complete package (frontend + backend + telegram bot)
|
|
- Frontend: Build frontend + backend files only
|
|
- Backend: Copy backend files only (no frontend build)
|
|
- TelegramBot: Build telegram bot package only
|
|
- If omitted: Interactive menu will be displayed
|
|
|
|
.PARAMETER OutputPath
|
|
Output path for deployment package (default: ./deploy-package)
|
|
|
|
.PARAMETER ServerHost
|
|
Remote server hostname/IP for automatic deployment (optional)
|
|
|
|
.PARAMETER ServerPath
|
|
Remote server path for automatic deployment (optional)
|
|
|
|
.PARAMETER Clean
|
|
Clean output directory before building (default: true)
|
|
|
|
.PARAMETER CleanCache
|
|
Clean build cache (cached node_modules) and exit
|
|
|
|
.EXAMPLE
|
|
.\Build-ROA2WEB.ps1
|
|
Shows interactive menu to select components to build
|
|
|
|
.EXAMPLE
|
|
.\Build-ROA2WEB.ps1 -Component All
|
|
Build complete deployment package (all components) without menu
|
|
|
|
.EXAMPLE
|
|
.\Build-ROA2WEB.ps1 -Component Frontend
|
|
Build only frontend + backend files
|
|
|
|
.EXAMPLE
|
|
.\Build-ROA2WEB.ps1 -Component TelegramBot
|
|
Build only Telegram bot package
|
|
|
|
.EXAMPLE
|
|
.\Build-ROA2WEB.ps1 -Component All -OutputPath "D:\deployments\roa2web-$(Get-Date -Format 'yyyyMMdd')"
|
|
Build complete package to custom output path
|
|
|
|
.EXAMPLE
|
|
.\Build-ROA2WEB.ps1 -CleanCache
|
|
Clean build cache to free disk space
|
|
|
|
.NOTES
|
|
Author: ROA2WEB Team
|
|
Version: 2.0 (Unified Build Script)
|
|
Requires: Node.js 16+ (for Frontend), Python 3.11+ (for validation)
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet("All", "Frontend", "Backend")]
|
|
[string]$Component = "",
|
|
|
|
[string]$OutputPath = "./deploy-package",
|
|
[string]$ServerHost = "",
|
|
[string]$ServerPath = "",
|
|
[bool]$Clean = $true,
|
|
[switch]$CleanCache
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# =============================================================================
|
|
# CONFIGURATION
|
|
# =============================================================================
|
|
|
|
$config = @{
|
|
# Ultrathin Monolith sources (relative to deployment/windows/ directory)
|
|
BackendSource = "../../backend"
|
|
FrontendSource = "../../src"
|
|
SharedSource = "../../shared"
|
|
ConfigSource = "../config"
|
|
RequiredNodeVersion = 16
|
|
}
|
|
|
|
# =============================================================================
|
|
# 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 Write-Info {
|
|
param([string]$Message)
|
|
Write-Host " [INFO] $Message" -ForegroundColor Gray
|
|
}
|
|
|
|
function Resolve-FullPath {
|
|
param([string]$Path)
|
|
|
|
$scriptDir = Split-Path -Parent $PSScriptRoot
|
|
$fullPath = Join-Path $scriptDir $Path
|
|
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
|
|
|
|
return $fullPath
|
|
}
|
|
|
|
function Clear-BuildCache {
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " CLEAN BUILD CACHE" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
|
|
$scriptDir = Split-Path -Parent $PSScriptRoot
|
|
$cacheDir = Join-Path $scriptDir ".build-cache"
|
|
|
|
if (-not (Test-Path $cacheDir)) {
|
|
Write-Warning "No build cache found at: $cacheDir"
|
|
Write-Host "`nNothing to clean." -ForegroundColor Gray
|
|
return
|
|
}
|
|
|
|
try {
|
|
# Calculate cache size
|
|
$cacheFiles = Get-ChildItem -Path $cacheDir -Recurse -File -ErrorAction SilentlyContinue
|
|
$cacheSize = ($cacheFiles | Measure-Object -Property Length -Sum).Sum / 1MB
|
|
|
|
Write-Host "`nCache location: $cacheDir" -ForegroundColor Gray
|
|
Write-Host "Cache size: $([math]::Round($cacheSize, 2)) MB" -ForegroundColor Gray
|
|
Write-Host "Files: $(($cacheFiles).Count)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
|
|
Write-Host "Are you sure you want to delete the build cache? [Y/N]: " -ForegroundColor Yellow -NoNewline
|
|
$confirmation = Read-Host
|
|
|
|
if ($confirmation.ToUpper() -eq "Y") {
|
|
Write-Step "Removing build cache..."
|
|
Remove-Item -Path $cacheDir -Recurse -Force -ErrorAction Stop
|
|
Write-Success "Build cache cleared successfully"
|
|
Write-Success "Freed $([math]::Round($cacheSize, 2)) MB of disk space"
|
|
} else {
|
|
Write-Host "`nCache cleanup cancelled." -ForegroundColor Yellow
|
|
}
|
|
} catch {
|
|
Write-Host "`n[ERROR] Failed to clear cache: $_" -ForegroundColor Red
|
|
}
|
|
}
|
|
|
|
function Show-BuildMenu {
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " ROA2WEB - Build Component Selection Menu" -ForegroundColor Cyan
|
|
Write-Host " Ultrathin Monolith Architecture" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " Select components to build:" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host " [1] All Components" -ForegroundColor White
|
|
Write-Host " (Unified Backend + Frontend)" -ForegroundColor Gray
|
|
Write-Host " Includes: Reports, Data Entry, and Telegram modules" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
Write-Host " [2] Frontend + Backend" -ForegroundColor White
|
|
Write-Host " (Vue.js SPA build + Unified FastAPI backend)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " [3] Backend Only" -ForegroundColor White
|
|
Write-Host " (Unified FastAPI backend + shared modules)" -ForegroundColor Gray
|
|
Write-Host " All modules included - control via MODULE_*_ENABLED flags" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
Write-Host " [C] Clean Build Cache" -ForegroundColor Yellow
|
|
Write-Host " (Remove cached node_modules to free disk space)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " [Q] Quit" -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
|
|
do {
|
|
Write-Host "`nYour choice: " -ForegroundColor Yellow -NoNewline
|
|
$choice = Read-Host
|
|
|
|
switch ($choice.ToUpper()) {
|
|
"1" { return "All" }
|
|
"2" { return "Frontend" }
|
|
"3" { return "Backend" }
|
|
"C" {
|
|
Clear-BuildCache
|
|
Write-Host "`nPress any key to return to menu..." -ForegroundColor Gray
|
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
return Show-BuildMenu
|
|
}
|
|
"Q" {
|
|
Write-Host "`nBuild cancelled by user." -ForegroundColor Yellow
|
|
exit 0
|
|
}
|
|
default {
|
|
Write-Host "Invalid choice. Please select 1-3, C or Q." -ForegroundColor Red
|
|
}
|
|
}
|
|
} while ($true)
|
|
}
|
|
|
|
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
|
|
if ($nodeVersion -match "v(\d+)\.") {
|
|
$major = [int]$matches[1]
|
|
if ($major -lt $config.RequiredNodeVersion) {
|
|
throw "Node.js version $($config.RequiredNodeVersion)+ 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: $($config.RequiredNodeVersion).x" -ForegroundColor Yellow
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Build-Frontend {
|
|
param(
|
|
[string]$SourcePath,
|
|
[string]$OutputPath
|
|
)
|
|
|
|
Write-Step "Building Vue.js frontend in isolated environment..."
|
|
|
|
if (-not (Test-Path $SourcePath)) {
|
|
throw "Frontend source path not found: $SourcePath"
|
|
}
|
|
|
|
# For ultrathin monolith, frontend sources are in src/ directory
|
|
# but package.json and vite.config.js are in project root
|
|
$appFolder = "roa2web-ultrathin-monolith"
|
|
|
|
# Create temporary build directory - this is the project root
|
|
# Structure:
|
|
# .temp-frontend-build/
|
|
# |- package.json (from project root)
|
|
# |- vite.config.js (from project root)
|
|
# |- src/ (frontend source)
|
|
# \- shared/
|
|
$tempRootDir = Join-Path $OutputPath ".temp-frontend-build"
|
|
if (Test-Path $tempRootDir) {
|
|
Write-Step "Cleaning existing temp build directory..."
|
|
Remove-Item -Path $tempRootDir -Recurse -Force
|
|
}
|
|
|
|
# Create temp root (this will be where npm install runs)
|
|
New-Item -ItemType Directory -Path $tempRootDir -Force | Out-Null
|
|
|
|
# Create src subdirectory
|
|
$tempSrcDir = Join-Path $tempRootDir "src"
|
|
New-Item -ItemType Directory -Path $tempSrcDir -Force | Out-Null
|
|
Write-Success "Created temp build directory (isolated from WSL)"
|
|
|
|
# The build directory is the root, not src
|
|
$tempBuildDir = $tempRootDir
|
|
|
|
# Create cache directory for node_modules (OUTSIDE deploy-package)
|
|
$scriptDir = Split-Path -Parent $PSScriptRoot
|
|
$cacheDir = Join-Path $scriptDir ".build-cache-$appFolder"
|
|
if (-not (Test-Path $cacheDir)) {
|
|
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
|
}
|
|
|
|
# Copy src/ contents to temp src/ subdirectory
|
|
Write-Step "Copying frontend sources to temp directory..."
|
|
$excludeDirs = @("node_modules", "dist", ".git", "__pycache__", ".vscode", ".idea")
|
|
$excludeFiles = @(".env", ".env.local", "*.log")
|
|
|
|
Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($SourcePath.Length).TrimStart('\', '/')
|
|
|
|
# Check if in excluded directory
|
|
$inExcludedDir = $false
|
|
foreach ($excludeDir in $excludeDirs) {
|
|
if ($relativePath -match "^$excludeDir" -or $relativePath -match "[\\/]$excludeDir[\\/]") {
|
|
$inExcludedDir = $true
|
|
break
|
|
}
|
|
}
|
|
if ($inExcludedDir) { return }
|
|
|
|
# Check if excluded file
|
|
$isExcludedFile = $false
|
|
foreach ($pattern in $excludeFiles) {
|
|
if ($_.Name -like $pattern) {
|
|
$isExcludedFile = $true
|
|
break
|
|
}
|
|
}
|
|
if ($isExcludedFile) { return }
|
|
|
|
# Copy to src/ subdirectory
|
|
$destPath = Join-Path $tempSrcDir $relativePath
|
|
|
|
if ($_.PSIsContainer) {
|
|
if (-not (Test-Path $destPath)) {
|
|
New-Item -ItemType Directory -Path $destPath -Force | Out-Null
|
|
}
|
|
} else {
|
|
$destDir = Split-Path $destPath -Parent
|
|
if (-not (Test-Path $destDir)) {
|
|
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
|
}
|
|
Copy-Item -Path $_.FullName -Destination $destPath -Force
|
|
}
|
|
}
|
|
Write-Success "Frontend sources copied to temp"
|
|
|
|
# Copy package.json, vite.config.js, package-lock.json, index.html from project root
|
|
Write-Step "Copying build configuration files..."
|
|
$projectRoot = Split-Path $SourcePath -Parent
|
|
$configFiles = @("package.json", "vite.config.js", "package-lock.json", "index.html")
|
|
foreach ($file in $configFiles) {
|
|
$srcFile = Join-Path $projectRoot $file
|
|
if (Test-Path $srcFile) {
|
|
$destFile = Join-Path $tempBuildDir $file
|
|
Copy-Item -Path $srcFile -Destination $destFile -Force
|
|
Write-Success "Copied: $file"
|
|
} else {
|
|
Write-Warning "Not found: $file"
|
|
}
|
|
}
|
|
|
|
# Copy public/ folder (contains web.config and other static assets)
|
|
$publicSourcePath = Join-Path $projectRoot "public"
|
|
if (Test-Path $publicSourcePath) {
|
|
$publicDestPath = Join-Path $tempBuildDir "public"
|
|
Write-Step "Copying public/ folder..."
|
|
Write-Info "Source: $publicSourcePath"
|
|
Write-Info "Dest: $publicDestPath"
|
|
Copy-Item -Path $publicSourcePath -Destination $publicDestPath -Recurse -Force
|
|
Write-Success "public/ folder copied (includes web.config)"
|
|
|
|
# Verify web.config was copied
|
|
$copiedWebConfig = Join-Path $publicDestPath "web.config"
|
|
if (Test-Path $copiedWebConfig) {
|
|
Write-Success "web.config found in public/"
|
|
} else {
|
|
Write-Warning "web.config NOT found in public/ - check source"
|
|
}
|
|
} else {
|
|
Write-Warning "public/ folder not found at: $publicSourcePath"
|
|
}
|
|
|
|
# Copy shared folder to maintain relative imports (../shared/)
|
|
# For ultrathin monolith: src/ and shared/ are siblings at project root
|
|
$projectRoot = Split-Path $SourcePath -Parent
|
|
$sharedSourcePath = Join-Path $projectRoot "shared"
|
|
|
|
if (Test-Path $sharedSourcePath) {
|
|
$sharedDestPath = Join-Path $tempRootDir "shared"
|
|
|
|
Write-Step "Copying shared components for build..."
|
|
Write-Info "Source: $sharedSourcePath"
|
|
Write-Info "Dest: $sharedDestPath"
|
|
|
|
# Remove existing shared folder in dest if exists
|
|
if (Test-Path $sharedDestPath) {
|
|
Remove-Item -Path $sharedDestPath -Recurse -Force
|
|
}
|
|
|
|
Copy-Item -Path $sharedSourcePath -Destination $sharedDestPath -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests")
|
|
Write-Success "Shared components copied for relative imports"
|
|
} else {
|
|
Write-Warning "Shared folder not found at: $sharedSourcePath"
|
|
}
|
|
|
|
# Build in temp directory (now at tempRootDir/src)
|
|
Push-Location $tempBuildDir
|
|
try {
|
|
# Check if dependencies need to be reinstalled
|
|
$needsInstall = $true
|
|
$cachedNodeModules = Join-Path $cacheDir "node_modules"
|
|
$cachedPackageJson = Join-Path $cacheDir "package.json"
|
|
$cachedPackageLock = Join-Path $cacheDir "package-lock.json"
|
|
|
|
$currentPackageJson = Join-Path $tempBuildDir "package.json"
|
|
$currentPackageLock = Join-Path $tempBuildDir "package-lock.json"
|
|
|
|
if ((Test-Path $cachedNodeModules) -and (Test-Path $cachedPackageJson)) {
|
|
# Compare package.json hashes
|
|
$currentHash = (Get-FileHash $currentPackageJson -Algorithm SHA256).Hash
|
|
$cachedHash = (Get-FileHash $cachedPackageJson -Algorithm SHA256).Hash
|
|
|
|
# Also compare package-lock.json if it exists
|
|
$lockMatches = $true
|
|
if ((Test-Path $currentPackageLock) -and (Test-Path $cachedPackageLock)) {
|
|
$currentLockHash = (Get-FileHash $currentPackageLock -Algorithm SHA256).Hash
|
|
$cachedLockHash = (Get-FileHash $cachedPackageLock -Algorithm SHA256).Hash
|
|
$lockMatches = ($currentLockHash -eq $cachedLockHash)
|
|
}
|
|
|
|
if (($currentHash -eq $cachedHash) -and $lockMatches) {
|
|
Write-Step "Reusing cached node_modules (dependencies unchanged)..."
|
|
Copy-Item -Path $cachedNodeModules -Destination $tempBuildDir -Recurse -Force
|
|
$needsInstall = $false
|
|
Write-Success "Dependencies restored from cache (saved 2-5 minutes!)"
|
|
} else {
|
|
Write-Info "Dependencies changed, will reinstall"
|
|
}
|
|
}
|
|
|
|
if ($needsInstall) {
|
|
# Install dependencies (including devDependencies for Vite)
|
|
Write-Step "Installing npm dependencies (Windows binaries)..."
|
|
# Clear NODE_ENV to ensure devDependencies (vite, etc.) are installed
|
|
Remove-Item Env:\NODE_ENV -ErrorAction SilentlyContinue
|
|
npm install | Out-Default
|
|
|
|
$nodeModulesPath = Join-Path $tempBuildDir "node_modules"
|
|
if (-not (Test-Path $nodeModulesPath)) {
|
|
throw "npm install failed: node_modules not created"
|
|
}
|
|
Write-Success "Dependencies installed in temp"
|
|
|
|
# Update cache
|
|
Write-Step "Caching node_modules for future builds..."
|
|
if (Test-Path $cachedNodeModules) {
|
|
Remove-Item -Path $cachedNodeModules -Recurse -Force
|
|
}
|
|
Copy-Item -Path $nodeModulesPath -Destination $cachedNodeModules -Recurse -Force
|
|
Copy-Item -Path $currentPackageJson -Destination $cachedPackageJson -Force
|
|
if (Test-Path $currentPackageLock) {
|
|
Copy-Item -Path $currentPackageLock -Destination $cachedPackageLock -Force
|
|
}
|
|
Write-Success "Cache updated for next build"
|
|
}
|
|
|
|
# Verify Vite is installed
|
|
$nodeModulesPath = Join-Path $tempBuildDir "node_modules"
|
|
$vitePath = Join-Path $nodeModulesPath ".bin\vite.cmd"
|
|
if (-not (Test-Path $vitePath)) {
|
|
throw "Vite not found in node_modules - devDependencies not installed"
|
|
}
|
|
|
|
# node_modules is already in tempBuildDir (which is tempRootDir)
|
|
# No need for junction since we build from project root
|
|
|
|
# Build for production
|
|
Write-Step "Building for production..."
|
|
$env:NODE_ENV = "production"
|
|
npm run build | Out-Default
|
|
Write-Success "Build completed"
|
|
|
|
# Verify dist folder
|
|
$distPath = Join-Path $tempBuildDir "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 ($([math]::Round($totalSize, 2)) MB)"
|
|
|
|
# Verify web.config was built
|
|
$webConfigPath = Join-Path $distPath "web.config"
|
|
if (Test-Path $webConfigPath) {
|
|
Write-Success "web.config found in build output"
|
|
} else {
|
|
Write-Warning "web.config NOT found in build output: $webConfigPath"
|
|
Write-Warning "Check if public/web.config exists in source"
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
if (-not (Test-Path $DestPath)) {
|
|
New-Item -ItemType Directory -Path $DestPath -Force | Out-Null
|
|
}
|
|
|
|
# Exclude patterns
|
|
$excludeDirs = @("venv", "__pycache__", ".pytest_cache", "logs", "temp", "node_modules")
|
|
$excludeFiles = @("*.pyc", "*.pyo", "*.log", ".env", ".env.local")
|
|
|
|
$normalizedSourcePath = $SourcePath.TrimEnd('\', '/') + '\'
|
|
|
|
$testExclude = {
|
|
param([string]$RelativePath, [bool]$IsDirectory, [array]$ExcludeDirs, [array]$ExcludeFiles)
|
|
|
|
if ($IsDirectory) {
|
|
$dirName = Split-Path $RelativePath -Leaf
|
|
if ($ExcludeDirs -contains $dirName) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
$pathParts = $RelativePath -split '[\\/]'
|
|
foreach ($part in $pathParts) {
|
|
if ($ExcludeDirs -contains $part) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
if (-not $IsDirectory) {
|
|
foreach ($pattern in $ExcludeFiles) {
|
|
if ($RelativePath -like $pattern) {
|
|
return $true
|
|
}
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object {
|
|
if ($_.FullName.Length -le $normalizedSourcePath.Length) {
|
|
return
|
|
}
|
|
$relativePath = $_.FullName.Substring($normalizedSourcePath.Length)
|
|
|
|
$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"
|
|
|
|
# Copy .env.example explicitly (excluded from recursive copy)
|
|
$sourceEnvExample = Join-Path $SourcePath ".env.example"
|
|
$destEnvExample = Join-Path $DestPath ".env.example"
|
|
if (Test-Path $sourceEnvExample) {
|
|
Copy-Item -Path $sourceEnvExample -Destination $destEnvExample -Force
|
|
Write-Success ".env.example template copied"
|
|
} else {
|
|
Write-Warning ".env.example not found in source - manual configuration required"
|
|
}
|
|
|
|
# Verify requirements.txt
|
|
$requirementsTxt = Join-Path $DestPath "requirements.txt"
|
|
if (-not (Test-Path $requirementsTxt)) {
|
|
Write-Error "CRITICAL: requirements.txt not found!"
|
|
throw "Backend package incomplete - missing requirements.txt"
|
|
}
|
|
Write-Success "Verified: requirements.txt present"
|
|
}
|
|
|
|
# Copy-TelegramBotFiles function removed - Telegram is now part of unified backend (backend/modules/telegram/)
|
|
|
|
# Copy-DataEntryBackendFiles function removed - Data Entry is now part of unified backend (backend/modules/data_entry/)
|
|
|
|
function Copy-SharedModules {
|
|
param(
|
|
[string]$SourcePath,
|
|
[string]$DestPath
|
|
)
|
|
|
|
Write-Step "Copying shared modules..."
|
|
|
|
if (Test-Path $SourcePath) {
|
|
Copy-Item -Path $SourcePath -Destination $DestPath -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests")
|
|
Write-Success "Shared modules copied"
|
|
} else {
|
|
Write-Warning "Shared modules not found at: $SourcePath"
|
|
}
|
|
}
|
|
|
|
function Copy-ConfigTemplates {
|
|
param(
|
|
[string]$SourcePath,
|
|
[string]$DestPath
|
|
)
|
|
|
|
Write-Step "Copying config templates..."
|
|
|
|
if (Test-Path $SourcePath) {
|
|
Copy-Item -Path $SourcePath -Destination $DestPath -Recurse -Force
|
|
Write-Success "Config templates copied"
|
|
}
|
|
}
|
|
|
|
function Remove-TempDirectories {
|
|
param([string]$OutputPath)
|
|
|
|
Write-Step "Cleaning up temporary build directories..."
|
|
|
|
$tempBuildDir = Join-Path $OutputPath ".temp-frontend-build"
|
|
|
|
if (Test-Path $tempBuildDir) {
|
|
try {
|
|
Remove-Item -Path $tempBuildDir -Recurse -Force -ErrorAction Stop
|
|
Write-Success "Temporary build directory cleaned"
|
|
} catch {
|
|
Write-Warning "Could not remove temp directory: $_"
|
|
Write-Warning "You may need to manually delete: $tempBuildDir"
|
|
}
|
|
} else {
|
|
Write-Success "No temporary directories to clean"
|
|
}
|
|
}
|
|
|
|
function Copy-DeploymentScripts {
|
|
param(
|
|
[string]$ScriptsSourcePath,
|
|
[string]$DestPath,
|
|
[string]$ComponentType
|
|
)
|
|
|
|
Write-Step "Copying deployment scripts..."
|
|
|
|
$scriptsDir = Join-Path $DestPath "scripts"
|
|
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
|
|
|
|
# Debug: Show paths
|
|
Write-Host " [DEBUG] ScriptsSourcePath: $ScriptsSourcePath" -ForegroundColor Magenta
|
|
Write-Host " [DEBUG] DestPath: $DestPath" -ForegroundColor Magenta
|
|
Write-Host " [DEBUG] ScriptsDir: $scriptsDir" -ForegroundColor Magenta
|
|
|
|
# Essential scripts for all deployments
|
|
$scripts = @(
|
|
"ROA2WEB-Console.ps1", # Unified deployment & management console
|
|
"Install-ROA2WEB.ps1",
|
|
"Install-TelegramBot.ps1"
|
|
)
|
|
|
|
# Add utility scripts for complete deployments
|
|
if ($ComponentType -eq "All" -or $ComponentType -eq "TelegramBot") {
|
|
$scripts += @(
|
|
"Backup-TelegramDB.ps1",
|
|
"Setup-DailyBackup.ps1",
|
|
"Setup-ClaudeAuth.ps1"
|
|
)
|
|
}
|
|
|
|
if ($ComponentType -eq "All" -or $ComponentType -eq "Frontend") {
|
|
$scripts += @(
|
|
"Enable-HTTPS.ps1"
|
|
)
|
|
}
|
|
|
|
$copiedCount = 0
|
|
foreach ($script in $scripts) {
|
|
$scriptPath = Join-Path $ScriptsSourcePath $script
|
|
if (Test-Path $scriptPath) {
|
|
$destScript = Join-Path $scriptsDir $script
|
|
|
|
# Debug: Show file info
|
|
$sourceSize = (Get-Item $scriptPath).Length
|
|
Write-Host " [DEBUG] Copying $script (size: $sourceSize bytes)" -ForegroundColor Magenta
|
|
|
|
# Force delete destination first to avoid caching issues
|
|
if (Test-Path $destScript) {
|
|
$oldSize = (Get-Item $destScript).Length
|
|
Write-Host " [DEBUG] Deleting old $script (was: $oldSize bytes)" -ForegroundColor Magenta
|
|
Remove-Item -Path $destScript -Force
|
|
}
|
|
|
|
Copy-Item -Path $scriptPath -Destination $scriptsDir -Force
|
|
|
|
# Debug: Verify copy
|
|
$newSize = (Get-Item $destScript).Length
|
|
Write-Host " [DEBUG] Copied $script (new size: $newSize bytes)" -ForegroundColor Magenta
|
|
|
|
$copiedCount++
|
|
} else {
|
|
Write-Warning "Script not found: $script"
|
|
}
|
|
}
|
|
|
|
Write-Success "Copied $copiedCount deployment scripts"
|
|
}
|
|
|
|
function New-DeploymentReadme {
|
|
param(
|
|
[string]$DestPath,
|
|
[string]$ComponentType
|
|
)
|
|
|
|
Write-Step "Creating deployment README..."
|
|
|
|
$componentDesc = switch ($ComponentType) {
|
|
"All" { "COMPLETE DEPLOYMENT PACKAGE (Ultrathin Monolith - All Modules)" }
|
|
"Frontend" { "UNIFIED FRONTEND + BACKEND DEPLOYMENT PACKAGE" }
|
|
"Backend" { "UNIFIED BACKEND DEPLOYMENT PACKAGE (All Modules)" }
|
|
}
|
|
|
|
$readme = @"
|
|
================================================================================
|
|
ROA2WEB DEPLOYMENT PACKAGE
|
|
$componentDesc
|
|
Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
|
================================================================================
|
|
|
|
CONTENTS:
|
|
---------
|
|
|
|
backend/ Unified FastAPI backend (port 8000)
|
|
|- modules/
|
|
| |- reports/ Reports module (Oracle)
|
|
| |- data_entry/ Data Entry module (SQLite)
|
|
| \- telegram/ Telegram bot module (background task)
|
|
\- main.py Single entry point (uvicorn)
|
|
|
|
frontend/ Unified Vue.js SPA (production build)
|
|
Single-page application with integrated modules
|
|
|
|
shared/ Shared Python modules
|
|
|- auth/ JWT authentication & middleware
|
|
|- database/ Oracle connection pool
|
|
\- routes/ Shared API routes (companies, calendar)
|
|
|
|
config/ Configuration templates
|
|
\- web.config IIS configuration for unified backend
|
|
|
|
scripts/ PowerShell deployment scripts
|
|
|
|
MODULE CONTROL:
|
|
---------------
|
|
Enable/disable modules via environment variables in backend/.env:
|
|
MODULE_REPORTS_ENABLED=true # Reports module
|
|
MODULE_DATA_ENTRY_ENABLED=true # Data Entry module
|
|
MODULE_TELEGRAM_ENABLED=true # Telegram bot module
|
|
|
|
|
|
DEPLOYMENT SCRIPTS:
|
|
-------------------
|
|
ROA2WEB-Console.ps1 Unified deployment & management console
|
|
- Deploy components (Backend/Frontend)
|
|
- Manage single unified service
|
|
- Check system status and health
|
|
|
|
Install-ROA2WEB.ps1 First-time installation (creates Windows service)
|
|
|
|
Enable-HTTPS.ps1 Configure HTTPS/SSL certificates
|
|
"@
|
|
|
|
$readme += @"
|
|
|
|
|
|
================================================================================
|
|
DEPLOYMENT WORKFLOW
|
|
================================================================================
|
|
|
|
>> FIRST TIME INSTALLATION:
|
|
---------------------------
|
|
|
|
1. Install Application (creates Windows service):
|
|
cd scripts
|
|
.\Install-ROA2WEB.ps1
|
|
|
|
2. Configure environment (.env file):
|
|
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
|
|
|
IMPORTANT - Configure module flags:
|
|
MODULE_REPORTS_ENABLED=true # Enable/disable Reports module
|
|
MODULE_DATA_ENTRY_ENABLED=true # Enable/disable Data Entry module
|
|
MODULE_TELEGRAM_ENABLED=true # Enable/disable Telegram bot module
|
|
|
|
3. Start the unified backend service:
|
|
Start-Service ROA2WEB-Backend
|
|
|
|
OR using console:
|
|
.\ROA2WEB-Console.ps1
|
|
(Select: Manage Services > Start Service)
|
|
|
|
4. Verify installation:
|
|
- Backend API: http://localhost:8000/docs
|
|
- Health check: http://localhost:8000/health
|
|
- Frontend: http://localhost/ (via IIS)
|
|
|
|
|
|
>> UPDATES (Deploy New Version):
|
|
----------------------------------
|
|
.\ROA2WEB-Console.ps1 -NonInteractive -Action DeployAll
|
|
(Stops service, updates files, restarts service)
|
|
|
|
>> SERVICE MANAGEMENT:
|
|
-----------------------
|
|
# Start service
|
|
Start-Service ROA2WEB-Backend
|
|
|
|
# Stop service
|
|
Stop-Service ROA2WEB-Backend
|
|
|
|
# Restart service
|
|
Restart-Service ROA2WEB-Backend
|
|
|
|
# Check status
|
|
Get-Service ROA2WEB-Backend
|
|
.\ROA2WEB-Console.ps1 -NonInteractive -Action Status
|
|
|
|
================================================================================
|
|
REQUIREMENTS
|
|
================================================================================
|
|
- Windows Server 2016+ or Windows 10/11
|
|
- IIS with URL Rewrite Module
|
|
- Python 3.11+
|
|
- PowerShell 5.1+ (run as Administrator)
|
|
|
|
NOTES:
|
|
------
|
|
- Virtual environments created automatically during installation
|
|
- .env files preserved during updates
|
|
- Automatic backup before each update
|
|
- Default install location: C:\inetpub\wwwroot\roa2web\
|
|
- Build cache NOT included in this package (stays on build machine)
|
|
- Single Windows service: ROA2WEB-Backend (manages all modules)
|
|
|
|
ARCHITECTURE:
|
|
-------------
|
|
ULTRATHIN MONOLITH: One backend process with multiple modules
|
|
- Modules controlled via .env flags (MODULE_*_ENABLED)
|
|
- All modules share: Oracle pool, auth, cache
|
|
- Telegram bot runs as background task (not separate service)
|
|
|
|
TROUBLESHOOTING:
|
|
----------------
|
|
Backend logs: C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log
|
|
C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log
|
|
IIS logs: C:\inetpub\logs\LogFiles\
|
|
Service status: Get-Service ROA2WEB-Backend
|
|
|
|
For detailed documentation, see: deployment/windows/docs/WINDOWS_DEPLOYMENT.md
|
|
|
|
================================================================================
|
|
"@
|
|
|
|
$readmePath = Join-Path $DestPath "README.txt"
|
|
Set-Content -Path $readmePath -Value $readme -Force
|
|
Write-Success "Deployment README created"
|
|
}
|
|
|
|
function New-DeploymentPackage {
|
|
param(
|
|
[string]$OutputPath,
|
|
[string]$ComponentType,
|
|
[hashtable]$Paths
|
|
)
|
|
|
|
Write-Step "Creating deployment package structure..."
|
|
|
|
# Clean output if requested
|
|
if ($Clean -and (Test-Path $OutputPath)) {
|
|
Write-Warning "Cleaning output directory..."
|
|
Remove-Item -Path $OutputPath -Recurse -Force
|
|
}
|
|
|
|
if (-not (Test-Path $OutputPath)) {
|
|
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
|
|
}
|
|
|
|
# Build based on component type
|
|
switch ($ComponentType) {
|
|
"All" {
|
|
# Unified Frontend (Vue.js SPA)
|
|
$frontendDistPath = Build-Frontend -SourcePath $Paths.FrontendSource -OutputPath $OutputPath
|
|
$frontendDest = Join-Path $OutputPath "frontend"
|
|
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
|
|
Write-Step "Copying Unified Frontend files (SPA)..."
|
|
Write-Info "Source: $frontendDistPath"
|
|
Write-Info "Destination: $frontendDest"
|
|
Copy-Item -Path "$frontendDistPath\*" -Destination $frontendDest -Recurse -Force
|
|
|
|
# Verify web.config was copied
|
|
$copiedWebConfig = Join-Path $frontendDest "web.config"
|
|
if (Test-Path $copiedWebConfig) {
|
|
Write-Success "web.config copied to package"
|
|
} else {
|
|
Write-Warning "web.config NOT copied to package"
|
|
}
|
|
|
|
Write-Success "Unified Frontend files copied"
|
|
|
|
# Unified Backend (includes Reports, Data Entry, Telegram modules)
|
|
$backendDest = Join-Path $OutputPath "backend"
|
|
Copy-BackendFiles -SourcePath $Paths.BackendSource -DestPath $backendDest
|
|
|
|
# Shared modules
|
|
$sharedDest = Join-Path $OutputPath "shared"
|
|
Copy-SharedModules -SourcePath $Paths.SharedSource -DestPath $sharedDest
|
|
|
|
# Config templates
|
|
$configDest = Join-Path $OutputPath "config"
|
|
Copy-ConfigTemplates -SourcePath $Paths.ConfigSource -DestPath $configDest
|
|
}
|
|
|
|
"Frontend" {
|
|
# Unified Frontend build (Vue.js SPA)
|
|
$frontendDistPath = Build-Frontend -SourcePath $Paths.FrontendSource -OutputPath $OutputPath
|
|
$frontendDest = Join-Path $OutputPath "frontend"
|
|
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
|
|
Write-Step "Copying Unified Frontend files (SPA)..."
|
|
Write-Info "Source: $frontendDistPath"
|
|
Write-Info "Destination: $frontendDest"
|
|
Copy-Item -Path "$frontendDistPath\*" -Destination $frontendDest -Recurse -Force
|
|
|
|
# Verify web.config was copied
|
|
$copiedWebConfig = Join-Path $frontendDest "web.config"
|
|
if (Test-Path $copiedWebConfig) {
|
|
Write-Success "web.config copied to package"
|
|
} else {
|
|
Write-Warning "web.config NOT copied to package"
|
|
}
|
|
|
|
Write-Success "Unified Frontend files copied"
|
|
|
|
# Unified Backend (includes all modules)
|
|
$backendDest = Join-Path $OutputPath "backend"
|
|
Copy-BackendFiles -SourcePath $Paths.BackendSource -DestPath $backendDest
|
|
|
|
# Shared modules
|
|
$sharedDest = Join-Path $OutputPath "shared"
|
|
Copy-SharedModules -SourcePath $Paths.SharedSource -DestPath $sharedDest
|
|
|
|
# Config templates
|
|
$configDest = Join-Path $OutputPath "config"
|
|
Copy-ConfigTemplates -SourcePath $Paths.ConfigSource -DestPath $configDest
|
|
}
|
|
|
|
"Backend" {
|
|
# Unified Backend only (includes Reports, Data Entry, Telegram modules)
|
|
$backendDest = Join-Path $OutputPath "backend"
|
|
Copy-BackendFiles -SourcePath $Paths.BackendSource -DestPath $backendDest
|
|
|
|
# Shared modules
|
|
$sharedDest = Join-Path $OutputPath "shared"
|
|
Copy-SharedModules -SourcePath $Paths.SharedSource -DestPath $sharedDest
|
|
|
|
# Config templates
|
|
$configDest = Join-Path $OutputPath "config"
|
|
Copy-ConfigTemplates -SourcePath $Paths.ConfigSource -DestPath $configDest
|
|
}
|
|
}
|
|
|
|
# Cleanup temporary directories
|
|
if ($ComponentType -eq "All" -or $ComponentType -eq "Frontend") {
|
|
Remove-TempDirectories -OutputPath $OutputPath
|
|
}
|
|
|
|
# Copy deployment scripts
|
|
Copy-DeploymentScripts -ScriptsSourcePath $PSScriptRoot -DestPath $OutputPath -ComponentType $ComponentType
|
|
|
|
# Create README
|
|
New-DeploymentReadme -DestPath $OutputPath -ComponentType $ComponentType
|
|
|
|
# Calculate package size
|
|
$packageFiles = Get-ChildItem -Path $OutputPath -Recurse -File
|
|
$packageSize = ($packageFiles | Measure-Object -Property Length -Sum).Sum / 1MB
|
|
|
|
Write-Success "Deployment package created"
|
|
Write-Success "Total files: $(($packageFiles).Count)"
|
|
Write-Success "Total size: $([math]::Round($packageSize, 2)) MB"
|
|
|
|
return $OutputPath
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN BUILD FLOW
|
|
# =============================================================================
|
|
|
|
function Main {
|
|
# Handle -CleanCache parameter
|
|
if ($CleanCache) {
|
|
Clear-BuildCache
|
|
exit 0
|
|
}
|
|
|
|
# Show interactive menu if no component specified
|
|
if ([string]::IsNullOrWhiteSpace($Component)) {
|
|
$script:Component = Show-BuildMenu
|
|
}
|
|
|
|
$banner = @"
|
|
|
|
====================================================================
|
|
ROA2WEB - Unified Build Script (v2.0)
|
|
Building: $Component
|
|
====================================================================
|
|
|
|
"@
|
|
Write-Host $banner -ForegroundColor Cyan
|
|
|
|
try {
|
|
# Resolve paths
|
|
$paths = @{
|
|
# Ultrathin Monolith sources
|
|
BackendSource = Resolve-FullPath -Path $config.BackendSource
|
|
FrontendSource = Resolve-FullPath -Path $config.FrontendSource
|
|
SharedSource = Resolve-FullPath -Path $config.SharedSource
|
|
ConfigSource = Resolve-FullPath -Path $config.ConfigSource
|
|
}
|
|
|
|
$outputFullPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Resolve-FullPath -Path $OutputPath }
|
|
|
|
Write-Host "`nConfiguration:" -ForegroundColor Yellow
|
|
Write-Host " Component: $Component"
|
|
Write-Host " Output Path: $outputFullPath"
|
|
|
|
# Validate Node.js for frontend builds
|
|
if ($Component -eq "All" -or $Component -eq "Frontend" -or $Component -eq "DataEntryApp") {
|
|
Test-NodeJS
|
|
}
|
|
|
|
# Build package
|
|
$packagePath = New-DeploymentPackage `
|
|
-OutputPath $outputFullPath `
|
|
-ComponentType $Component `
|
|
-Paths $paths
|
|
|
|
# Show success
|
|
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
|
|
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
|
Write-Host " 1. Transfer package to Windows Server" -ForegroundColor Gray
|
|
Write-Host " 2. On server, run deployment scripts from scripts/ directory" -ForegroundColor Gray
|
|
Write-Host " 3. Use Manage-ROA2WEB.ps1 for service management" -ForegroundColor Gray
|
|
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
|
|
} catch {
|
|
Write-Host "`n[BUILD FAILED] $_" -ForegroundColor Red
|
|
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Run main build
|
|
Main
|