- Add ssh-tunnel.ps1: Windows SSH tunnel manager (equivalent to ssh-tunnel.sh) - Supports password auth via plink.exe (PuTTY) - Supports ssh_hostkey for non-interactive batch mode - Commands: start, stop, restart, status - Add start-backend-service.ps1: NSSM service wrapper - Starts SSH tunnels before uvicorn - Waits for tunnel ports to be accessible (30s timeout) - Configured by Install-ROA2WEB.ps1 - Add start.ps1: Windows equivalent of start.sh - Orchestrates SSH tunnel + backend + frontend startup - Add backend/shared/ssh_tunnel_manager.py: Python monitoring - Background asyncio task monitors tunnel health every 30s - Auto-restarts tunnels after 2 consecutive failures - Exposes status to /health endpoint - Update ROA2WEB-Console.ps1: - Add Deploy-Scripts function - Update Update-ServiceToUseVenv to use wrapper script - Fix PowerShell reserved variable ($PID -> $tunnelPid) - Fix script path detection (scripts/ vs deployment/windows/scripts/) - Update README.md with ssh_hostkey documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1492 lines
54 KiB
PowerShell
1492 lines
54 KiB
PowerShell
<#
|
|
.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", "CheckOCR", "InstallOCR")]
|
|
[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"
|
|
# IMPORTANT: venv is OUTSIDE roa2web to survive deployments!
|
|
VenvPath = "C:\inetpub\wwwroot\roa2web-venv"
|
|
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 Get-PythonPaths {
|
|
<#
|
|
.SYNOPSIS
|
|
Returns Python and pip paths, preferring venv if available
|
|
#>
|
|
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
|
|
$venvPip = Join-Path $Config.VenvPath "Scripts\pip.exe"
|
|
|
|
if (Test-Path $venvPython) {
|
|
return @{
|
|
Python = $venvPython
|
|
Pip = $venvPip
|
|
IsVenv = $true
|
|
}
|
|
} else {
|
|
# Fallback to global Python
|
|
$globalPython = (Get-Command python -ErrorAction SilentlyContinue).Source
|
|
$globalPip = (Get-Command pip -ErrorAction SilentlyContinue).Source
|
|
return @{
|
|
Python = $globalPython
|
|
Pip = $globalPip
|
|
IsVenv = $false
|
|
}
|
|
}
|
|
}
|
|
|
|
function Initialize-Venv {
|
|
<#
|
|
.SYNOPSIS
|
|
Creates virtual environment at external location if it doesn't exist.
|
|
External location (C:\inetpub\wwwroot\roa2web-venv) survives deployments.
|
|
#>
|
|
$venvPath = $Config.VenvPath
|
|
$venvPython = Join-Path $venvPath "Scripts\python.exe"
|
|
$venvPip = Join-Path $venvPath "Scripts\pip.exe"
|
|
|
|
# If venv exists and is valid (pip works), we're good
|
|
if (Test-Path $venvPython) {
|
|
# Verify pip is functional (not broken by move)
|
|
try {
|
|
$pipTest = & $venvPip --version 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Success "Virtual environment already exists at $venvPath"
|
|
return $true
|
|
} else {
|
|
Write-Warning "Venv exists but pip is broken, recreating..."
|
|
Remove-Item -Path $venvPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
} catch {
|
|
Write-Warning "Venv exists but pip test failed, recreating..."
|
|
Remove-Item -Path $venvPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
Write-Step "Creating virtual environment at external location..."
|
|
Write-Info "Path: $venvPath (survives deployments)"
|
|
try {
|
|
$globalPython = (Get-Command python -ErrorAction Stop).Source
|
|
& $globalPython -m venv $venvPath
|
|
if (Test-Path $venvPython) {
|
|
Write-Success "Virtual environment created"
|
|
|
|
# Upgrade pip
|
|
Write-Step "Upgrading pip in venv..."
|
|
& $venvPython -m pip install --upgrade pip 2>&1 | Out-Null
|
|
Write-Success "Pip upgraded"
|
|
|
|
return $true
|
|
} else {
|
|
Write-Error "Failed to create virtual environment"
|
|
return $false
|
|
}
|
|
} catch {
|
|
Write-Error "Failed to create venv: $_"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Update-ServiceToUseVenv {
|
|
<#
|
|
.SYNOPSIS
|
|
Updates NSSM service to use wrapper script for SSH tunnel auto-start
|
|
|
|
.DESCRIPTION
|
|
Configures the service to use start-backend-service.ps1 wrapper which:
|
|
1. Starts SSH tunnels before backend
|
|
2. Waits for tunnel ports to be accessible
|
|
3. Starts uvicorn with correct settings
|
|
|
|
This ensures SSH tunnels are always running when the backend starts.
|
|
#>
|
|
$venvPython = Join-Path $Config.VenvPath "Scripts\python.exe"
|
|
|
|
if (-not (Test-Path $venvPython)) {
|
|
Write-Warning "Venv Python not found: $venvPython"
|
|
return $false
|
|
}
|
|
|
|
# Check if nssm is available
|
|
$nssmPath = Get-Command nssm -ErrorAction SilentlyContinue
|
|
if (-not $nssmPath) {
|
|
Write-Warning "NSSM not found in PATH"
|
|
return $false
|
|
}
|
|
|
|
# Find wrapper script
|
|
$wrapperScript = Join-Path $Config.InstallRoot "scripts\start-backend-service.ps1"
|
|
if (-not (Test-Path $wrapperScript)) {
|
|
# Fallback: try deployment location
|
|
$wrapperScript = Join-Path $PSScriptRoot "start-backend-service.ps1"
|
|
}
|
|
|
|
$useWrapper = Test-Path $wrapperScript
|
|
|
|
try {
|
|
# Get current application
|
|
$currentApp = & nssm get $Config.ServiceName Application 2>&1
|
|
|
|
if ($useWrapper) {
|
|
# Check if already using wrapper
|
|
if ($currentApp -like "*powershell*") {
|
|
$currentArgs = & nssm get $Config.ServiceName AppParameters 2>&1
|
|
if ($currentArgs -like "*start-backend-service.ps1*") {
|
|
Write-Success "Service already configured to use wrapper script"
|
|
return $true
|
|
}
|
|
}
|
|
|
|
Write-Step "Updating service to use wrapper script (SSH tunnel auto-start)..."
|
|
|
|
# Stop service first
|
|
Stop-ROAService | Out-Null
|
|
|
|
# Update service to use PowerShell wrapper
|
|
& nssm set $Config.ServiceName Application "powershell.exe"
|
|
& nssm set $Config.ServiceName AppParameters "-ExecutionPolicy Bypass -File `"$wrapperScript`""
|
|
|
|
Write-Success "Service updated to use wrapper: $wrapperScript"
|
|
Write-Info "SSH tunnels will auto-start when service starts"
|
|
} else {
|
|
# Fallback: use venv Python directly (old behavior)
|
|
if ($currentApp -eq $venvPython) {
|
|
Write-Success "Service already configured to use venv Python"
|
|
return $true
|
|
}
|
|
|
|
Write-Step "Updating service to use venv Python (no wrapper available)..."
|
|
|
|
# Stop service first
|
|
Stop-ROAService | Out-Null
|
|
|
|
# Update service application
|
|
& nssm set $Config.ServiceName Application $venvPython
|
|
& nssm set $Config.ServiceName AppParameters "-m uvicorn main:app --host 127.0.0.1 --port $($Config.ServicePort) --workers 1"
|
|
|
|
Write-Success "Service updated to use: $venvPython"
|
|
Write-Warning "Wrapper script not found - SSH tunnels must be started manually"
|
|
}
|
|
|
|
return $true
|
|
} catch {
|
|
Write-Error "Failed to update service: $_"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# OCR DEPENDENCY CHECK
|
|
# =============================================================================
|
|
|
|
function Test-OCRDependencies {
|
|
param(
|
|
[switch]$AutoInstall,
|
|
[switch]$Silent
|
|
)
|
|
|
|
if (-not $Silent) {
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " OCR Dependencies Check$(if ($AutoInstall) { ' (Auto-Install Enabled)' })" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
}
|
|
|
|
$allOk = $true
|
|
|
|
# Get Python paths (prefer venv)
|
|
$pyPaths = Get-PythonPaths
|
|
|
|
# Check Python/venv
|
|
if (-not $Silent) { Write-Step "Checking Python installation..." }
|
|
|
|
if ($pyPaths.IsVenv) {
|
|
if (-not $Silent) {
|
|
$venvPythonVersion = & $pyPaths.Python --version 2>&1
|
|
Write-Success "Virtual environment: $($Config.VenvPath)"
|
|
Write-Success "Python (venv): $venvPythonVersion"
|
|
}
|
|
# Ensure service uses venv Python
|
|
if ($AutoInstall) {
|
|
# Install requirements.txt if exists
|
|
$requirementsFile = Join-Path $Config.BackendPath "requirements.txt"
|
|
if (Test-Path $requirementsFile) {
|
|
if (-not $Silent) { Write-Step "Installing base requirements in venv..." }
|
|
try {
|
|
& $pyPaths.Pip install -r $requirementsFile 2>&1 | Out-Null
|
|
if (-not $Silent) { Write-Success "Base requirements installed" }
|
|
} catch {
|
|
if (-not $Silent) { Write-Warning "Failed to install requirements: $_" }
|
|
}
|
|
}
|
|
|
|
if (-not $Silent) { Write-Step "Ensuring service uses venv Python..." }
|
|
$serviceUpdated = Update-ServiceToUseVenv
|
|
if ($serviceUpdated -and -not $Silent) {
|
|
Write-Success "Service configured to use venv"
|
|
}
|
|
}
|
|
} else {
|
|
# No venv - create one if AutoInstall
|
|
if ($AutoInstall) {
|
|
if (-not $Silent) { Write-Warning "Virtual environment not found - creating..." }
|
|
$venvCreated = Initialize-Venv
|
|
if ($venvCreated) {
|
|
# Refresh paths
|
|
$pyPaths = Get-PythonPaths
|
|
|
|
# Update service to use venv Python
|
|
if (-not $Silent) { Write-Step "Updating service to use virtual environment..." }
|
|
Update-ServiceToUseVenv | Out-Null
|
|
} else {
|
|
if (-not $Silent) { Write-Error "Could not create virtual environment" }
|
|
return $false
|
|
}
|
|
} else {
|
|
# Check global Python
|
|
if ($pyPaths.Python) {
|
|
$pythonVersion = cmd /c "python --version 2>&1"
|
|
if (-not $Silent) {
|
|
Write-Warning "Using global Python (venv recommended)"
|
|
Write-Success "Python: $pythonVersion"
|
|
}
|
|
} else {
|
|
if (-not $Silent) { Write-Error "Python not found" }
|
|
return $false
|
|
}
|
|
}
|
|
}
|
|
|
|
# Determine pip executable to use
|
|
$pipExe = if ($pyPaths.Pip -and (Test-Path $pyPaths.Pip)) { "`"$($pyPaths.Pip)`"" } else { "pip" }
|
|
if (-not $Silent -and $pyPaths.IsVenv) { Write-Info "Using pip from venv: $pipExe" }
|
|
|
|
# Check and optionally install Python packages
|
|
if (-not $Silent) { Write-Step "Checking Python OCR packages..." }
|
|
|
|
$packages = @(
|
|
@{ Name = "torch"; Package = "torch"; Required = $true; Description = "PyTorch (for docTR)" },
|
|
@{ Name = "torchvision"; Package = "torchvision"; Required = $true; Description = "TorchVision (for docTR)" },
|
|
@{ Name = "python-doctr"; Package = "python-doctr"; Required = $true; Description = "docTR OCR engine" },
|
|
@{ Name = "pytesseract"; Package = "pytesseract"; Required = $true; Description = "Tesseract Python wrapper" },
|
|
@{ Name = "paddleocr"; Package = "paddleocr"; Required = $true; Description = "PaddleOCR engine" }
|
|
)
|
|
|
|
foreach ($pkg in $packages) {
|
|
# Use venv pip to check packages
|
|
$pipOutput = cmd /c "$pipExe show $($pkg.Package) 2>&1"
|
|
$isInstalled = $pipOutput -match "Version:"
|
|
|
|
if ($isInstalled) {
|
|
$versionLine = $pipOutput | Where-Object { $_ -match "^Version:" }
|
|
$version = if ($versionLine) { ($versionLine -split ":")[1].Trim() } else { "unknown" }
|
|
if (-not $Silent) { Write-Success "$($pkg.Package): $version" }
|
|
} else {
|
|
if ($pkg.Required) {
|
|
if ($AutoInstall) {
|
|
if (-not $Silent) { Write-Warning "$($pkg.Package): NOT INSTALLED - Installing..." }
|
|
try {
|
|
# Use venv pip to install
|
|
$installCmd = "$pipExe install `"$($pkg.Name)`""
|
|
if (-not $Silent) { Write-Info " Running: $installCmd" }
|
|
$installResult = Invoke-Expression "cmd /c $installCmd 2>&1"
|
|
# Verify installation
|
|
$verifyOutput = cmd /c "$pipExe show $($pkg.Package) 2>&1"
|
|
if ($verifyOutput -match "Version:") {
|
|
if (-not $Silent) { Write-Success "$($pkg.Package): Installed successfully" }
|
|
} else {
|
|
if (-not $Silent) {
|
|
Write-Error "$($pkg.Package): Installation FAILED"
|
|
# Show last few lines of pip output for debugging
|
|
$errorLines = ($installResult | Select-Object -Last 5) -join "`n"
|
|
if ($errorLines) {
|
|
Write-Host " Pip output:" -ForegroundColor Gray
|
|
Write-Host " $errorLines" -ForegroundColor Gray
|
|
}
|
|
Write-Info " Try manually: $pipExe install `"$($pkg.Name)`""
|
|
}
|
|
$allOk = $false
|
|
}
|
|
} catch {
|
|
if (-not $Silent) { Write-Error "$($pkg.Package): Installation error - $_" }
|
|
$allOk = $false
|
|
}
|
|
} else {
|
|
if (-not $Silent) {
|
|
Write-Error "$($pkg.Package): NOT INSTALLED - $($pkg.Description)"
|
|
Write-Info " Install with: pip install $($pkg.Name)"
|
|
}
|
|
$allOk = $false
|
|
}
|
|
} else {
|
|
if (-not $Silent) { Write-Warning "$($pkg.Package): Not installed (optional)" }
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check external tools
|
|
if (-not $Silent) { Write-Step "Checking external OCR tools..." }
|
|
|
|
# Check for Chocolatey (used for auto-install)
|
|
$chocoAvailable = $null -ne (Get-Command choco -ErrorAction SilentlyContinue)
|
|
|
|
# Install Chocolatey if needed and AutoInstall is enabled
|
|
if ($AutoInstall -and -not $chocoAvailable) {
|
|
if (-not $Silent) { Write-Warning "Chocolatey: NOT FOUND - Installing..." }
|
|
try {
|
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
|
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
|
# Refresh environment
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
$chocoAvailable = $null -ne (Get-Command choco -ErrorAction SilentlyContinue)
|
|
if ($chocoAvailable) {
|
|
if (-not $Silent) { Write-Success "Chocolatey: Installed successfully" }
|
|
} else {
|
|
if (-not $Silent) { Write-Warning "Chocolatey: Installed but not in PATH - restart PowerShell" }
|
|
}
|
|
} catch {
|
|
if (-not $Silent) { Write-Error "Chocolatey: Installation failed - $_" }
|
|
}
|
|
}
|
|
|
|
# Tesseract
|
|
$tesseractPath = Get-Command tesseract -ErrorAction SilentlyContinue
|
|
if ($tesseractPath) {
|
|
$tessVersion = cmd /c "tesseract --version 2>&1" | Select-Object -First 1
|
|
if (-not $Silent) {
|
|
Write-Success "Tesseract: $tessVersion"
|
|
Write-Info " Path: $($tesseractPath.Source)"
|
|
}
|
|
} else {
|
|
if ($AutoInstall) {
|
|
if ($chocoAvailable) {
|
|
if (-not $Silent) { Write-Warning "Tesseract: NOT FOUND - Installing via Chocolatey..." }
|
|
try {
|
|
$result = cmd /c "choco install tesseract -y 2>&1"
|
|
# Refresh PATH
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
$tesseractPath = Get-Command tesseract -ErrorAction SilentlyContinue
|
|
if ($tesseractPath) {
|
|
if (-not $Silent) { Write-Success "Tesseract: Installed successfully" }
|
|
} else {
|
|
if (-not $Silent) { Write-Error "Tesseract: Installation completed but not in PATH - restart PowerShell" }
|
|
$allOk = $false
|
|
}
|
|
} catch {
|
|
if (-not $Silent) { Write-Error "Tesseract: Chocolatey install failed - $_" }
|
|
$allOk = $false
|
|
}
|
|
} else {
|
|
if (-not $Silent) {
|
|
Write-Error "Tesseract: NOT FOUND - Chocolatey not available for auto-install"
|
|
Write-Info " Install Chocolatey first: https://chocolatey.org/install"
|
|
Write-Info " Then run: choco install tesseract -y"
|
|
}
|
|
$allOk = $false
|
|
}
|
|
} else {
|
|
if (-not $Silent) {
|
|
Write-Error "Tesseract: NOT FOUND in PATH"
|
|
Write-Info " Install with: choco install tesseract -y"
|
|
Write-Info " Or download from: https://github.com/UB-Mannheim/tesseract/wiki"
|
|
}
|
|
$allOk = $false
|
|
}
|
|
}
|
|
|
|
# Poppler (for PDF support)
|
|
$popplerPath = Get-Command pdftoppm -ErrorAction SilentlyContinue
|
|
if ($popplerPath) {
|
|
if (-not $Silent) {
|
|
Write-Success "Poppler: Found (pdftoppm)"
|
|
Write-Info " Path: $($popplerPath.Source)"
|
|
}
|
|
} else {
|
|
if ($AutoInstall -and $chocoAvailable) {
|
|
if (-not $Silent) { Write-Warning "Poppler: NOT FOUND - Installing via Chocolatey..." }
|
|
try {
|
|
$result = cmd /c "choco install poppler -y 2>&1"
|
|
# Refresh PATH
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
$popplerPath = Get-Command pdftoppm -ErrorAction SilentlyContinue
|
|
if ($popplerPath) {
|
|
if (-not $Silent) { Write-Success "Poppler: Installed successfully" }
|
|
} else {
|
|
if (-not $Silent) { Write-Warning "Poppler: Installation completed but not in PATH - restart PowerShell" }
|
|
}
|
|
} catch {
|
|
if (-not $Silent) { Write-Warning "Poppler: Chocolatey install failed - $_" }
|
|
}
|
|
} else {
|
|
if (-not $Silent) {
|
|
Write-Warning "Poppler: NOT FOUND in PATH (required for PDF OCR)"
|
|
Write-Info " Install with: choco install poppler -y"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check .env OCR settings
|
|
if (-not $Silent) {
|
|
Write-Step "Checking OCR configuration in .env..."
|
|
$envPath = Join-Path $Config.BackendPath ".env"
|
|
if (Test-Path $envPath) {
|
|
$envContent = Get-Content $envPath -Raw
|
|
|
|
$ocrSettings = @(
|
|
"OCR_ENABLE_PADDLEOCR",
|
|
"OCR_ENABLE_TESSERACT",
|
|
"OCR_DEFAULT_ENGINE",
|
|
"OCR_WORKERS"
|
|
)
|
|
|
|
foreach ($setting in $ocrSettings) {
|
|
if ($envContent -match "$setting\s*=\s*(.+)") {
|
|
Write-Info " $setting = $($Matches[1].Trim())"
|
|
} else {
|
|
Write-Warning " ${setting}: NOT CONFIGURED"
|
|
}
|
|
}
|
|
} else {
|
|
Write-Warning ".env file not found at: $envPath"
|
|
}
|
|
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
if ($allOk) {
|
|
Write-Host " Result: All required OCR dependencies are installed" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " Result: Some required dependencies are MISSING" -ForegroundColor Red
|
|
Write-Host " Run with -AutoInstall to install missing packages" -ForegroundColor Yellow
|
|
}
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
}
|
|
|
|
return $allOk
|
|
}
|
|
|
|
# =============================================================================
|
|
# 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-Scripts {
|
|
<#
|
|
.SYNOPSIS
|
|
Deploys PowerShell scripts to installation directory
|
|
|
|
.DESCRIPTION
|
|
Copies deployment scripts from package to install root's scripts folder.
|
|
This includes the wrapper script needed for SSH tunnel auto-start.
|
|
#>
|
|
param([string]$SourcePath)
|
|
|
|
Write-Step "Deploying scripts..."
|
|
|
|
$sourceScripts = Join-Path $SourcePath "scripts"
|
|
$destScripts = Join-Path $Config.InstallRoot "scripts"
|
|
|
|
# Scripts to deploy (essential for operation)
|
|
$requiredScripts = @(
|
|
"ssh-tunnel.ps1",
|
|
"start-backend-service.ps1",
|
|
"ROA2WEB-Console.ps1"
|
|
)
|
|
|
|
try {
|
|
# Create scripts directory if needed
|
|
if (-not (Test-Path $destScripts)) {
|
|
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
|
|
Write-Info "Created scripts directory: $destScripts"
|
|
}
|
|
|
|
$deployedCount = 0
|
|
|
|
# Copy scripts from package
|
|
if (Test-Path $sourceScripts) {
|
|
foreach ($script in $requiredScripts) {
|
|
$srcFile = Join-Path $sourceScripts $script
|
|
$destFile = Join-Path $destScripts $script
|
|
|
|
if (Test-Path $srcFile) {
|
|
Copy-Item -Path $srcFile -Destination $destFile -Force
|
|
Write-Info "Deployed: $script"
|
|
$deployedCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
# Also copy from current script location (fallback)
|
|
foreach ($script in $requiredScripts) {
|
|
$srcFile = Join-Path $PSScriptRoot $script
|
|
$destFile = Join-Path $destScripts $script
|
|
|
|
if ((Test-Path $srcFile) -and (-not (Test-Path $destFile))) {
|
|
Copy-Item -Path $srcFile -Destination $destFile -Force
|
|
Write-Info "Deployed (from PSScriptRoot): $script"
|
|
$deployedCount++
|
|
}
|
|
}
|
|
|
|
if ($deployedCount -gt 0) {
|
|
Write-Success "Scripts deployed ($deployedCount files)"
|
|
} else {
|
|
Write-Warning "No scripts to deploy"
|
|
}
|
|
|
|
# Verify essential wrapper script
|
|
$wrapperPath = Join-Path $destScripts "start-backend-service.ps1"
|
|
if (Test-Path $wrapperPath) {
|
|
Write-Success "Service wrapper script ready: $wrapperPath"
|
|
} else {
|
|
Write-Warning "Service wrapper script not found - SSH tunnel auto-start may not work"
|
|
}
|
|
|
|
return $true
|
|
} catch {
|
|
Write-Error "Scripts deployment failed: $_"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
# Preserve data directory (contains SQLite databases with production data!)
|
|
$dataDir = Join-Path $Config.BackendPath "data"
|
|
$dataTempPath = Join-Path $env:TEMP "roa2web-data-backup-$(Get-Date -Format 'yyyyMMddHHmmss')"
|
|
$dataBackup = $null
|
|
if (Test-Path $dataDir) {
|
|
Write-Info "Preserving data directory: receipts.db, telegram.db, cache, uploads"
|
|
Write-Info " - receipts/: Data Entry SQLite database (receipts, approvals)"
|
|
Write-Info " - telegram/: Telegram bot auth/session database"
|
|
Write-Info " - cache/: Reports L2 cache database"
|
|
Copy-Item -Path $dataDir -Destination $dataTempPath -Recurse -Force
|
|
$dataBackup = $dataTempPath
|
|
}
|
|
|
|
# Delete old venv inside backend if it exists (it has hardcoded paths and can't be moved)
|
|
$oldVenvPath = Join-Path $Config.BackendPath "venv"
|
|
if (Test-Path $oldVenvPath) {
|
|
Write-Info "Removing old venv from backend directory (will use external venv)"
|
|
Remove-Item -Path $oldVenvPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
# Restore data directory
|
|
if ($dataBackup -and (Test-Path $dataBackup)) {
|
|
# Remove the empty data dir from package and restore the preserved one
|
|
$newDataDir = Join-Path $Config.BackendPath "data"
|
|
if (Test-Path $newDataDir) {
|
|
Remove-Item -Path $newDataDir -Recurse -Force
|
|
}
|
|
Copy-Item -Path $dataBackup -Destination $newDataDir -Recurse -Force
|
|
Remove-Item -Path $dataBackup -Recurse -Force -ErrorAction SilentlyContinue
|
|
Write-Success "Data directory restored (SQLite databases preserved)"
|
|
}
|
|
} 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"
|
|
}
|
|
|
|
# Setup virtual environment
|
|
Write-Step "Setting up Python virtual environment..."
|
|
$venvCreated = Initialize-Venv
|
|
if (-not $venvCreated) {
|
|
Write-Warning "Could not create/verify virtual environment"
|
|
}
|
|
|
|
# Install requirements.txt
|
|
$requirementsFile = Join-Path $Config.BackendPath "requirements.txt"
|
|
if (Test-Path $requirementsFile) {
|
|
$pyPaths = Get-PythonPaths
|
|
if ($pyPaths.IsVenv) {
|
|
Write-Step "Installing Python dependencies in venv..."
|
|
$pipExe = $pyPaths.Pip
|
|
|
|
# Verify pip is functional before installing
|
|
$pipVersion = & $pipExe --version 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Error "Pip is not functional: $pipVersion"
|
|
Write-Info "Try: Remove-Item -Recurse $($Config.VenvPath); then redeploy"
|
|
} else {
|
|
Write-Info "Using pip: $pipVersion"
|
|
|
|
# Install requirements (ignore warnings, check exit code only)
|
|
$oldErrorAction = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
$pipOutput = & $pipExe install -r $requirementsFile 2>&1
|
|
$pipExitCode = $LASTEXITCODE
|
|
$ErrorActionPreference = $oldErrorAction
|
|
|
|
# Log warnings but don't fail on them
|
|
$pipOutput | ForEach-Object {
|
|
if ($_ -match "WARNING:") {
|
|
Write-Warning $_
|
|
}
|
|
}
|
|
|
|
if ($pipExitCode -eq 0) {
|
|
# Verify uvicorn installed (critical dependency)
|
|
$uvicornCheck = & $pipExe show uvicorn 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Success "Python dependencies installed"
|
|
} else {
|
|
Write-Error "Dependencies install failed - uvicorn not found"
|
|
Write-Info "Manual fix: $pipExe install -r $requirementsFile"
|
|
}
|
|
} else {
|
|
Write-Error "Pip install failed with exit code $pipExitCode"
|
|
Write-Info "Manual fix: $pipExe install -r $requirementsFile"
|
|
}
|
|
}
|
|
} else {
|
|
Write-Warning "No venv found - skipping requirements.txt installation"
|
|
}
|
|
}
|
|
|
|
# Update service to use venv Python
|
|
$pyPaths = Get-PythonPaths
|
|
if ($pyPaths.IsVenv) {
|
|
Update-ServiceToUseVenv | Out-Null
|
|
}
|
|
|
|
# Start service
|
|
Start-Sleep -Seconds 2
|
|
if (Start-ROAService) {
|
|
Write-Success "Backend deployment completed successfully"
|
|
|
|
# Check and auto-install OCR dependencies after deployment
|
|
Write-Step "Checking and installing OCR dependencies..."
|
|
$ocrOk = Test-OCRDependencies -AutoInstall
|
|
if (-not $ocrOk) {
|
|
Write-Warning "Some OCR dependencies could not be installed automatically"
|
|
Write-Info "Manual installation may be required for external tools (Tesseract, Poppler)"
|
|
}
|
|
|
|
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 scripts first (needed for service wrapper)
|
|
$scriptsOk = Deploy-Scripts -SourcePath $SourcePath
|
|
|
|
# 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 " Scripts: " -NoNewline
|
|
if ($scriptsOk) {
|
|
Write-Host "[OK] Success" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "[X] Failed" -ForegroundColor Red
|
|
}
|
|
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 ($scriptsOk -and $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 " [9] Check/Install OCR Dependencies" -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"
|
|
}
|
|
"9" {
|
|
$ocrOk = Test-OCRDependencies
|
|
if (-not $ocrOk) {
|
|
Write-Host ""
|
|
Write-Host " Install missing dependencies? (Y/N): " -ForegroundColor Yellow -NoNewline
|
|
$installChoice = Read-Host
|
|
if ($installChoice -eq "Y" -or $installChoice -eq "y") {
|
|
Write-Host ""
|
|
Test-OCRDependencies -AutoInstall | Out-Null
|
|
}
|
|
}
|
|
Wait-ForKeyPress
|
|
return "Continue"
|
|
}
|
|
"Q" { return "Quit" }
|
|
default {
|
|
Write-Host "Invalid choice. Please select 1-9 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
|
|
}
|
|
"CheckOCR" {
|
|
$success = Test-OCRDependencies
|
|
exit $(if ($success) { 0 } else { 1 })
|
|
}
|
|
"InstallOCR" {
|
|
$success = Test-OCRDependencies -AutoInstall
|
|
exit $(if ($success) { 0 } else { 1 })
|
|
}
|
|
}
|
|
}
|
|
|
|
# Interactive mode
|
|
do {
|
|
$result = Show-MainMenu
|
|
} while ($result -eq "Continue")
|
|
|
|
Write-Host "`nGoodbye!`n" -ForegroundColor Cyan
|
|
}
|
|
|
|
# Run main
|
|
Main
|