<# .SYNOPSIS ROA2WEB Unified Console - Ultrathin Monolith Management .DESCRIPTION Unified deployment and management console for ROA2WEB Ultrathin Monolith. Single Windows service with multiple modules (Reports, Data Entry, Telegram). Features: - Deploy backend/frontend updates - Manage unified service (start/stop/restart) - View logs and service status - Backup before deployment - Module control via .env flags .PARAMETER NonInteractive Run in non-interactive mode with specific action .PARAMETER Action Action to perform: - DeployBackend: Deploy backend files only - DeployFrontend: Deploy frontend files only - DeployAll: Deploy both backend and frontend - StartService: Start ROA2WEB-Backend service - StopService: Stop ROA2WEB-Backend service - RestartService: Restart ROA2WEB-Backend service - Status: Show service status and health - ViewLogs: Display recent log entries .PARAMETER PackagePath Path to deployment package (for Deploy actions) .EXAMPLE .\ROA2WEB-Console.ps1 Launch interactive menu .EXAMPLE .\ROA2WEB-Console.ps1 -NonInteractive -Action DeployAll -PackagePath "C:\Temp\deploy-20250129-120000" Deploy full package non-interactively .EXAMPLE .\ROA2WEB-Console.ps1 -NonInteractive -Action RestartService Restart the unified backend service .NOTES Author: ROA2WEB Team Version: 2.0 (Ultrathin Monolith) Requires: Administrator privileges, PowerShell 5.1+ #> [CmdletBinding()] param( [switch]$NonInteractive, [ValidateSet("DeployBackend", "DeployFrontend", "DeployAll", "StartService", "StopService", "RestartService", "Status", "ViewLogs")] [string]$Action = "", [string]$PackagePath = "" ) $ErrorActionPreference = "Stop" # Require Administrator if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "[ERROR] This script requires Administrator privileges" -ForegroundColor Red Write-Host "Please run PowerShell as Administrator and try again." -ForegroundColor Yellow exit 1 } # ============================================================================= # CONFIGURATION # ============================================================================= $script:Config = @{ # Service Configuration ServiceName = "ROA2WEB-Backend" ServiceDisplayName = "ROA2WEB Unified Backend Service" ServicePort = 8000 HealthUrl = "http://localhost:8000/health" HealthTimeout = 10 # Installation Paths InstallRoot = "C:\inetpub\wwwroot\roa2web" BackendPath = "C:\inetpub\wwwroot\roa2web\backend" FrontendPath = "C:\inetpub\wwwroot\roa2web\frontend" SharedPath = "C:\inetpub\wwwroot\roa2web\shared" ConfigPath = "C:\inetpub\wwwroot\roa2web\config" DataPath = "C:\inetpub\wwwroot\roa2web\data" # Logs LogsPath = "C:\inetpub\wwwroot\roa2web\logs" BackendStdoutLog = "C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log" BackendStderrLog = "C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log" # Backups BackupPath = "C:\inetpub\wwwroot\roa2web\backups" MaxBackups = 5 # IIS Configuration IISSiteName = "Default Web Site" IISAppName = "roa2web" } # ============================================================================= # 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 " [*] $Message" -ForegroundColor Gray } function Wait-ForKeyPress { Write-Host "`nPress any key to continue..." -ForegroundColor Gray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } # ============================================================================= # SERVICE MANAGEMENT # ============================================================================= function Get-ServiceSafe { param([string]$ServiceName) try { return Get-Service -Name $ServiceName -ErrorAction SilentlyContinue } catch { return $null } } function Test-ServiceInstalled { $service = Get-ServiceSafe -ServiceName $Config.ServiceName return ($null -ne $service) } function Start-ROAService { Write-Step "Starting $($Config.ServiceDisplayName)..." $service = Get-ServiceSafe -ServiceName $Config.ServiceName if (-not $service) { Write-Error "Service not found: $($Config.ServiceName)" Write-Info "Run Install-ROA2WEB.ps1 first to install the service" return $false } if ($service.Status -eq 'Running') { Write-Success "Service is already running" return $true } try { Start-Service -Name $Config.ServiceName Start-Sleep -Seconds 3 # Wait for service to start (max 30 seconds) $maxWait = 30 $waited = 0 while ($waited -lt $maxWait) { $service = Get-Service -Name $Config.ServiceName if ($service.Status -eq 'Running') { Write-Success "Service started successfully" # Wait a bit more for backend to initialize Write-Info "Waiting for backend initialization..." Start-Sleep -Seconds 5 # Test health endpoint Test-ServiceHealth | Out-Null return $true } Start-Sleep -Seconds 2 $waited += 2 } Write-Warning "Service started but status unclear" return $false } catch { Write-Error "Failed to start service: $_" return $false } } function Stop-ROAService { Write-Step "Stopping $($Config.ServiceDisplayName)..." $service = Get-ServiceSafe -ServiceName $Config.ServiceName if (-not $service) { Write-Warning "Service not found: $($Config.ServiceName)" return $true } if ($service.Status -eq 'Stopped') { Write-Success "Service is already stopped" return $true } try { Stop-Service -Name $Config.ServiceName -Force # Wait for service to stop (max 30 seconds) $maxWait = 30 $waited = 0 while ($waited -lt $maxWait) { $service = Get-Service -Name $Config.ServiceName if ($service.Status -eq 'Stopped') { Write-Success "Service stopped successfully" return $true } Start-Sleep -Seconds 2 $waited += 2 } Write-Warning "Service stop timeout - may still be running" return $false } catch { Write-Error "Failed to stop service: $_" return $false } } function Restart-ROAService { Write-Step "Restarting $($Config.ServiceDisplayName)..." if (Stop-ROAService) { Start-Sleep -Seconds 2 return Start-ROAService } return $false } function Test-ServiceHealth { Write-Step "Checking service health..." $service = Get-ServiceSafe -ServiceName $Config.ServiceName if (-not $service) { Write-Warning "Service not installed" return $false } Write-Info "Service Status: $($service.Status)" if ($service.Status -ne 'Running') { Write-Warning "Service is not running" return $false } # Test health endpoint try { Write-Info "Testing health endpoint: $($Config.HealthUrl)" $response = Invoke-WebRequest -Uri $Config.HealthUrl -TimeoutSec $Config.HealthTimeout -UseBasicParsing if ($response.StatusCode -eq 200) { Write-Success "Health check PASSED (HTTP 200)" # Parse response for module status try { $health = $response.Content | ConvertFrom-Json Write-Info "Modules Status:" if ($health.modules) { foreach ($module in $health.modules.PSObject.Properties) { $status = if ($module.Value) { "[ON] Enabled" } else { "[OFF] Disabled" } Write-Info " - $($module.Name): $status" } } } catch { # If can't parse JSON, just show raw response Write-Info "Response: $($response.Content)" } return $true } else { Write-Warning "Health check returned HTTP $($response.StatusCode)" return $false } } catch { Write-Warning "Health check FAILED: $_" Write-Info "This may indicate the backend is still initializing or there's a configuration issue" return $false } } # ============================================================================= # BACKUP FUNCTIONS # ============================================================================= function New-Backup { param([string]$Component) Write-Step "Creating backup before deployment..." if (-not (Test-Path $Config.BackupPath)) { New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null } $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $backupName = "backup-${Component}-${timestamp}" $backupDest = Join-Path $Config.BackupPath $backupName try { New-Item -ItemType Directory -Path $backupDest -Force | Out-Null if ($Component -eq "Backend" -or $Component -eq "All") { if (Test-Path $Config.BackendPath) { Write-Info "Backing up backend..." Copy-Item -Path $Config.BackendPath -Destination (Join-Path $backupDest "backend") -Recurse -Force } if (Test-Path $Config.SharedPath) { Write-Info "Backing up shared modules..." Copy-Item -Path $Config.SharedPath -Destination (Join-Path $backupDest "shared") -Recurse -Force } } if ($Component -eq "Frontend" -or $Component -eq "All") { if (Test-Path $Config.FrontendPath) { Write-Info "Backing up frontend..." Copy-Item -Path $Config.FrontendPath -Destination (Join-Path $backupDest "frontend") -Recurse -Force } } Write-Success "Backup created: $backupName" # Clean old backups (keep last N) $allBackups = Get-ChildItem -Path $Config.BackupPath -Directory | Sort-Object CreationTime -Descending if ($allBackups.Count -gt $Config.MaxBackups) { $toDelete = $allBackups | Select-Object -Skip $Config.MaxBackups foreach ($old in $toDelete) { Write-Info "Removing old backup: $($old.Name)" Remove-Item -Path $old.FullName -Recurse -Force } } return $true } catch { Write-Error "Backup failed: $_" return $false } } # ============================================================================= # DEPLOYMENT FUNCTIONS # ============================================================================= function Deploy-Backend { param([string]$SourcePath) Write-Step "Deploying backend..." $sourceBe = Join-Path $SourcePath "backend" $sourceShared = Join-Path $SourcePath "shared" if (-not (Test-Path $sourceBe)) { Write-Error "Backend not found in package: $sourceBe" return $false } try { # Stop service if (-not (Stop-ROAService)) { Write-Warning "Failed to stop service, continuing anyway..." } # Backup if (-not (New-Backup -Component "Backend")) { Write-Warning "Backup failed, but continuing with deployment" } # Deploy backend Write-Info "Copying backend files..." if (Test-Path $Config.BackendPath) { # Preserve .env file $envFile = Join-Path $Config.BackendPath ".env" $envBackup = $null if (Test-Path $envFile) { $envBackup = Get-Content $envFile -Raw Write-Info "Preserving .env file" } Remove-Item -Path $Config.BackendPath -Recurse -Force Copy-Item -Path $sourceBe -Destination $Config.BackendPath -Recurse -Force # Restore .env if ($envBackup) { Set-Content -Path $envFile -Value $envBackup -Force Write-Success ".env file restored" } } else { Copy-Item -Path $sourceBe -Destination $Config.BackendPath -Recurse -Force } Write-Success "Backend files deployed" # Deploy shared modules if present if (Test-Path $sourceShared) { Write-Info "Copying shared modules..." if (Test-Path $Config.SharedPath) { Remove-Item -Path $Config.SharedPath -Recurse -Force } Copy-Item -Path $sourceShared -Destination $Config.SharedPath -Recurse -Force Write-Success "Shared modules deployed" } # Start service Start-Sleep -Seconds 2 if (Start-ROAService) { Write-Success "Backend deployment completed successfully" return $true } else { Write-Warning "Backend deployed but service start failed" Write-Info "Check logs: $($Config.BackendStderrLog)" return $false } } catch { Write-Error "Backend deployment failed: $_" return $false } } function Deploy-Frontend { param([string]$SourcePath) Write-Step "Deploying frontend..." $sourceFe = Join-Path $SourcePath "frontend" if (-not (Test-Path $sourceFe)) { Write-Error "Frontend not found in package: $sourceFe" return $false } try { # Backup if (-not (New-Backup -Component "Frontend")) { Write-Warning "Backup failed, but continuing with deployment" } # Deploy frontend Write-Info "Copying frontend files to IIS..." if (Test-Path $Config.FrontendPath) { Remove-Item -Path $Config.FrontendPath -Recurse -Force } Copy-Item -Path $sourceFe -Destination $Config.FrontendPath -Recurse -Force Write-Success "Frontend files deployed" # Verify web.config was deployed (should be in frontend/ from dist/) $deployedWebConfig = Join-Path $Config.FrontendPath "web.config" if (Test-Path $deployedWebConfig) { Write-Success "web.config deployed successfully" # Verify it has correct configuration for /roa2web/ path $configContent = Get-Content $deployedWebConfig -Raw if ($configContent -match 'url="[^"]*roa2web/api') { Write-Success "web.config contains correct /roa2web/api proxy rules" } else { Write-Warning "web.config may not have correct /roa2web/api proxy configuration" } } else { Write-Warning "web.config not found in deployed frontend: $deployedWebConfig" Write-Warning "IIS reverse proxy will not work without web.config" Write-Warning "Ensure 'public/web.config' exists in source and rebuild frontend" } Write-Success "Frontend deployment completed successfully" return $true } catch { Write-Error "Frontend deployment failed: $_" return $false } } function Deploy-All { param([string]$SourcePath) Write-Step "Deploying complete package (Backend + Frontend)..." Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host " FULL DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan # Backup everything if (-not (New-Backup -Component "All")) { Write-Warning "Backup failed, but continuing with deployment" } # Deploy backend (includes service restart) $backendOk = Deploy-Backend -SourcePath $SourcePath # Deploy frontend $frontendOk = Deploy-Frontend -SourcePath $SourcePath Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host " DEPLOYMENT SUMMARY" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host " Backend: " -NoNewline if ($backendOk) { Write-Host "[OK] Success" -ForegroundColor Green } else { Write-Host "[X] Failed" -ForegroundColor Red } Write-Host " Frontend: " -NoNewline if ($frontendOk) { Write-Host "[OK] Success" -ForegroundColor Green } else { Write-Host "[X] Failed" -ForegroundColor Red } Write-Host ("=" * 70) -ForegroundColor Cyan return ($backendOk -and $frontendOk) } # ============================================================================= # LOG VIEWING # ============================================================================= function Show-Logs { Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host " Service Logs" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan # Backend stdout if (Test-Path $Config.BackendStdoutLog) { Write-Host "`n--- Backend STDOUT (last 30 lines) ---" -ForegroundColor Yellow Get-Content $Config.BackendStdoutLog -Tail 30 | ForEach-Object { Write-Host $_ -ForegroundColor Gray } } else { Write-Warning "Backend stdout log not found: $($Config.BackendStdoutLog)" } # Backend stderr if (Test-Path $Config.BackendStderrLog) { Write-Host "`n--- Backend STDERR (last 20 lines) ---" -ForegroundColor Yellow Get-Content $Config.BackendStderrLog -Tail 20 | ForEach-Object { Write-Host $_ -ForegroundColor Red } } else { Write-Info "Backend stderr log not found (this is OK if no errors)" } Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan } # ============================================================================= # STATUS DISPLAY # ============================================================================= function Show-Status { Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host " ROA2WEB Ultrathin Monolith - System Status" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan # Service Status $service = Get-ServiceSafe -ServiceName $Config.ServiceName Write-Host "`n Service: $($Config.ServiceDisplayName)" -ForegroundColor Yellow if ($service) { Write-Host " Status: " -NoNewline if ($service.Status -eq 'Running') { Write-Host "$($service.Status)" -ForegroundColor Green } else { Write-Host "$($service.Status)" -ForegroundColor Red } Write-Host " Name: $($Config.ServiceName)" -ForegroundColor Gray Write-Host " Port: $($Config.ServicePort)" -ForegroundColor Gray } else { Write-Host " Status: " -NoNewline Write-Host "Not Installed" -ForegroundColor Red Write-Host " Run Install-ROA2WEB.ps1 to install the service" -ForegroundColor Gray } # Health Check if ($service -and $service.Status -eq 'Running') { Write-Host "" Test-ServiceHealth | Out-Null } # Paths Write-Host "`n Installation Paths:" -ForegroundColor Yellow Write-Host " Root: $($Config.InstallRoot)" -ForegroundColor Gray Write-Host " Backend: $($Config.BackendPath)" -ForegroundColor Gray Write-Host " Frontend: $($Config.FrontendPath)" -ForegroundColor Gray Write-Host " Shared: $($Config.SharedPath)" -ForegroundColor Gray Write-Host " Logs: $($Config.LogsPath)" -ForegroundColor Gray # Module Configuration $envFile = Join-Path $Config.BackendPath ".env" if (Test-Path $envFile) { Write-Host "`n Module Configuration (.env):" -ForegroundColor Yellow $envContent = Get-Content $envFile $modules = $envContent | Where-Object { $_ -match "^MODULE_.*_ENABLED=" } foreach ($line in $modules) { if ($line -match "MODULE_(.+)_ENABLED=(.+)") { $moduleName = $matches[1] $enabled = $matches[2] -eq "true" Write-Host " $moduleName`: " -NoNewline -ForegroundColor Gray if ($enabled) { Write-Host "Enabled" -ForegroundColor Green } else { Write-Host "Disabled" -ForegroundColor Red } } } } # Endpoints Write-Host "`n API Endpoints:" -ForegroundColor Yellow Write-Host " Health: http://localhost:$($Config.ServicePort)/health" -ForegroundColor Gray Write-Host " Docs: http://localhost:$($Config.ServicePort)/docs" -ForegroundColor Gray Write-Host " Frontend: http://localhost/ (via IIS)" -ForegroundColor Gray Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan } # ============================================================================= # INTERACTIVE MENU # ============================================================================= function Show-MainMenu { Clear-Host Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host " ROA2WEB Unified Console - Ultrathin Monolith" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan $service = Get-ServiceSafe -ServiceName $Config.ServiceName if ($service) { Write-Host "`n Service Status: " -NoNewline if ($service.Status -eq 'Running') { Write-Host "RUNNING" -ForegroundColor Green } else { Write-Host "$($service.Status)" -ForegroundColor Yellow } } else { Write-Host "`n Service Status: " -NoNewline Write-Host "NOT INSTALLED" -ForegroundColor Red } Write-Host "`n Main Menu:" -ForegroundColor Yellow Write-Host "" Write-Host " === Deployment ===" -ForegroundColor Cyan Write-Host " [1] Deploy Backend" -ForegroundColor White Write-Host " [2] Deploy Frontend" -ForegroundColor White Write-Host " [3] Deploy All (Backend + Frontend)" -ForegroundColor White Write-Host "" Write-Host " === Service Management ===" -ForegroundColor Cyan Write-Host " [4] Start Service" -ForegroundColor White Write-Host " [5] Stop Service" -ForegroundColor White Write-Host " [6] Restart Service" -ForegroundColor White Write-Host "" Write-Host " === Monitoring ===" -ForegroundColor Cyan Write-Host " [7] View Status" -ForegroundColor White Write-Host " [8] View Logs" -ForegroundColor White 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" { Write-Host "`nEnter deployment package path: " -NoNewline $pkgPath = Read-Host if (Test-Path $pkgPath) { Deploy-Backend -SourcePath $pkgPath | Out-Null } else { Write-Error "Package path not found: $pkgPath" } Wait-ForKeyPress return "Continue" } "2" { Write-Host "`nEnter deployment package path: " -NoNewline $pkgPath = Read-Host if (Test-Path $pkgPath) { Deploy-Frontend -SourcePath $pkgPath | Out-Null } else { Write-Error "Package path not found: $pkgPath" } Wait-ForKeyPress return "Continue" } "3" { Write-Host "`nEnter deployment package path: " -NoNewline $pkgPath = Read-Host if (Test-Path $pkgPath) { Deploy-All -SourcePath $pkgPath | Out-Null } else { Write-Error "Package path not found: $pkgPath" } Wait-ForKeyPress return "Continue" } "4" { Start-ROAService | Out-Null Wait-ForKeyPress return "Continue" } "5" { Stop-ROAService | Out-Null Wait-ForKeyPress return "Continue" } "6" { Restart-ROAService | Out-Null Wait-ForKeyPress return "Continue" } "7" { Show-Status Wait-ForKeyPress return "Continue" } "8" { Show-Logs Wait-ForKeyPress return "Continue" } "Q" { return "Quit" } default { Write-Host "Invalid choice. Please select 1-8 or Q." -ForegroundColor Red } } } while ($true) } # ============================================================================= # MAIN EXECUTION # ============================================================================= function Main { # Check if service installed if (-not (Test-ServiceInstalled) -and -not $NonInteractive) { Write-Warning "ROA2WEB service not found" Write-Info "Run Install-ROA2WEB.ps1 first to install the service" Write-Host "" Wait-ForKeyPress } # Non-interactive mode if ($NonInteractive -and $Action) { switch ($Action) { "DeployBackend" { if (-not $PackagePath) { Write-Error "PackagePath parameter required for DeployBackend" exit 1 } $success = Deploy-Backend -SourcePath $PackagePath exit $(if ($success) { 0 } else { 1 }) } "DeployFrontend" { if (-not $PackagePath) { Write-Error "PackagePath parameter required for DeployFrontend" exit 1 } $success = Deploy-Frontend -SourcePath $PackagePath exit $(if ($success) { 0 } else { 1 }) } "DeployAll" { if (-not $PackagePath) { Write-Error "PackagePath parameter required for DeployAll" exit 1 } $success = Deploy-All -SourcePath $PackagePath exit $(if ($success) { 0 } else { 1 }) } "StartService" { $success = Start-ROAService exit $(if ($success) { 0 } else { 1 }) } "StopService" { $success = Stop-ROAService exit $(if ($success) { 0 } else { 1 }) } "RestartService" { $success = Restart-ROAService exit $(if ($success) { 0 } else { 1 }) } "Status" { Show-Status exit 0 } "ViewLogs" { Show-Logs exit 0 } } } # Interactive mode do { $result = Show-MainMenu } while ($result -eq "Continue") Write-Host "`nGoodbye!`n" -ForegroundColor Cyan } # Run main Main