feat: Improve Windows deployment and fix production paths

Data Entry App:
- Fix shared path finding for both dev and production environments
- Add base URL support for IIS subdirectory deployment (/data-entry/)
- Use import.meta.env.BASE_URL in router for correct path handling
- Add email-validator and python-jose dependencies

Deployment Scripts:
- Enhance Build-ROA2WEB.ps1 with improved build process
- Update ROA2WEB-Console.ps1 with Data Entry support
- Improve Publish-And-Deploy.ps1 deployment workflow
- Update deploy-config.json with new settings

Gitignore:
- Add more build cache patterns to ignore
- Add temp frontend build directories

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 19:44:15 +02:00
parent 642ae3a96c
commit 3295f60faa
10 changed files with 1015 additions and 62 deletions

View File

@@ -42,8 +42,9 @@
param(
[switch]$NonInteractive,
[ValidateSet("DeployBackend", "DeployTelegramBot", "DeployAll",
"StartAll", "StopAll", "RestartAll", "Status")]
[ValidateSet("DeployBackend", "DeployTelegramBot", "DeployDataEntry", "DeployAll",
"StartAll", "StopAll", "RestartAll",
"StartDataEntry", "StopDataEntry", "RestartDataEntry", "Status")]
[string]$Action = ""
)
@@ -72,16 +73,16 @@ $script:Config = @{
SourcePath = $detectedSourcePath
BackupPath = "C:\inetpub\wwwroot\roa2web\backups"
# Backend
# Reports Backend
BackendPath = "C:\inetpub\wwwroot\roa2web\backend"
BackendServiceName = "ROA2WEB-Backend"
BackendServiceDisplayName = "ROA2WEB Backend API"
BackendServiceDescription = "FastAPI backend service for ROA2WEB application"
BackendServiceDescription = "FastAPI backend service for ROA2WEB Reports application"
BackendPort = 8000
BackendHealthUrl = "http://localhost:8000/health"
BackendHealthTimeout = 5
# Frontend
# Reports Frontend
FrontendPath = "C:\inetpub\wwwroot\roa2web\frontend"
# IIS Configuration
@@ -98,9 +99,24 @@ $script:Config = @{
TelegramBotHealthUrl = "http://localhost:8002/internal/health"
TelegramBotHealthTimeout = 10
# Data Entry Backend
DataEntryBackendPath = "C:\inetpub\wwwroot\roa2web\data-entry-backend"
DataEntryServiceName = "ROA2WEB-DataEntry"
DataEntryServiceDisplayName = "ROA2WEB Data Entry API"
DataEntryServiceDescription = "FastAPI backend service for ROA2WEB Data Entry application"
DataEntryPort = 8003
DataEntryHealthUrl = "http://localhost:8003/health"
DataEntryHealthTimeout = 5
# Data Entry Frontend
DataEntryFrontendPath = "C:\inetpub\wwwroot\roa2web\data-entry-frontend"
DataEntryIISAppName = "data-entry"
DataEntryAppPoolName = "DataEntry-AppPool"
# Logs
LogsPath = "C:\inetpub\wwwroot\roa2web\logs"
TelegramBotLogsPath = "C:\inetpub\wwwroot\roa2web\telegram-bot\logs"
DataEntryLogsPath = "C:\inetpub\wwwroot\roa2web\data-entry-backend\logs"
# Timeouts
ServiceTimeout = 30
@@ -208,6 +224,36 @@ function Test-TelegramBotInstalled {
}
}
function Test-DataEntryInstalled {
Write-Step "Checking if Data Entry is installed..."
$venvPath = Join-Path $Config.DataEntryBackendPath "venv"
$venvExists = Test-Path $venvPath
$service = Get-ServiceSafe -ServiceName $Config.DataEntryServiceName
$serviceExists = $null -ne $service
$appPath = Join-Path $Config.DataEntryBackendPath "app"
$hasApp = Test-Path $appPath
$requirementsPath = Join-Path $Config.DataEntryBackendPath "requirements.txt"
$hasRequirements = Test-Path $requirementsPath
if ($venvExists -and $serviceExists -and $hasApp -and $hasRequirements) {
Write-Success "Data Entry is installed (venv + service exist)"
return $true
} else {
Write-Warning "Data Entry NOT installed (missing: $(
@(
if (-not $venvExists) { 'venv' }
if (-not $serviceExists) { 'service' }
if (-not $hasApp) { 'app files' }
if (-not $hasRequirements) { 'requirements.txt' }
) -join ', '
))"
return $false
}
}
function Get-ServiceSafe {
param([string]$ServiceName)
try {
@@ -859,6 +905,256 @@ function Install-TelegramBotFirstTime {
}
}
function Install-DataEntryFirstTime {
Write-Host "`n" + ("=" * 70) -ForegroundColor Yellow
Write-Host " FIRST-TIME INSTALLATION: DATA ENTRY APP" -ForegroundColor Yellow
Write-Host ("=" * 70) -ForegroundColor Yellow
try {
# Install prerequisites
Write-Step "Installing prerequisites..."
Install-Chocolatey | Out-Null
if (-not (Install-Python)) {
throw "Python installation failed"
}
if (-not (Install-NSSM)) {
throw "NSSM installation failed"
}
# Create directory structure
Write-Step "Creating directory structure..."
$directories = @(
$Config.DataEntryBackendPath,
$Config.DataEntryFrontendPath,
(Join-Path $Config.DataEntryBackendPath "app"),
(Join-Path $Config.DataEntryBackendPath "data"),
$Config.DataEntryLogsPath,
(Join-Path $Config.DataEntryBackendPath "temp"),
(Join-Path $Config.DataEntryBackendPath "migrations")
)
foreach ($dir in $directories) {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Write-Success "Created: $dir"
} else {
Write-Success "Already exists: $dir"
}
}
# Set permissions
try {
$acl = Get-Acl $Config.DataEntryBackendPath
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule("SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($systemRule)
Set-Acl -Path $Config.DataEntryBackendPath -AclObject $acl
$iisRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($iisRule)
Set-Acl -Path $Config.DataEntryFrontendPath -AclObject $acl
Write-Success "Permissions set for SYSTEM and IIS_IUSRS"
} catch {
Write-Warning "Could not set permissions: $_"
}
# Copy data entry backend files
Write-Step "Copying Data Entry backend files..."
$sourceDataEntryBackend = Join-Path $Config.SourcePath "data-entry-backend"
if (-not (Test-Path $sourceDataEntryBackend)) {
throw "Source data-entry-backend path not found: $sourceDataEntryBackend"
}
# Copy app directory
$sourceApp = Join-Path $sourceDataEntryBackend "app"
$destApp = Join-Path $Config.DataEntryBackendPath "app"
if (Test-Path $destApp) {
Remove-Item -Path $destApp -Recurse -Force
}
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
Write-Success "Application files copied"
# Copy migrations directory
$sourceMigrations = Join-Path $sourceDataEntryBackend "migrations"
$destMigrations = Join-Path $Config.DataEntryBackendPath "migrations"
if (Test-Path $sourceMigrations) {
if (Test-Path $destMigrations) {
Remove-Item -Path $destMigrations -Recurse -Force
}
Copy-Item -Path $sourceMigrations -Destination $destMigrations -Recurse -Force
Write-Success "Migrations copied"
}
# Copy alembic.ini
$sourceAlembic = Join-Path $sourceDataEntryBackend "alembic.ini"
if (Test-Path $sourceAlembic) {
Copy-Item -Path $sourceAlembic -Destination $Config.DataEntryBackendPath -Force
Write-Success "alembic.ini copied"
}
# Copy requirements.txt
$sourceReq = Join-Path $sourceDataEntryBackend "requirements.txt"
if (Test-Path $sourceReq) {
$destReq = Join-Path $Config.DataEntryBackendPath "requirements.txt"
Copy-Item -Path $sourceReq -Destination $destReq -Force
Write-Success "requirements.txt copied"
}
# Copy .env.example and .env.prod templates
foreach ($envFile in @(".env.example", ".env.prod", ".env.test")) {
$sourceEnv = Join-Path $sourceDataEntryBackend $envFile
if (Test-Path $sourceEnv) {
$destEnv = Join-Path $Config.DataEntryBackendPath $envFile
Copy-Item -Path $sourceEnv -Destination $destEnv -Force
Write-Success "$envFile template copied"
}
}
# Copy shared modules (always update to ensure latest version)
$sourceShared = Join-Path $Config.SourcePath "shared"
if (Test-Path $sourceShared) {
$destShared = Join-Path $Config.InstallPath "shared"
if (-not (Test-Path $destShared)) {
New-Item -ItemType Directory -Path $destShared -Force | Out-Null
}
# Always copy/update shared modules to ensure all subfolders are present
Copy-Item -Path "$sourceShared\*" -Destination $destShared -Recurse -Force -Exclude @("__pycache__", "*.pyc")
Write-Success "Shared modules updated"
}
# Copy frontend files
Write-Step "Copying Data Entry frontend files..."
$sourceDataEntryFrontend = Join-Path $Config.SourcePath "data-entry-frontend"
if (Test-Path $sourceDataEntryFrontend) {
Copy-Item -Path "$sourceDataEntryFrontend\*" -Destination $Config.DataEntryFrontendPath -Recurse -Force
Write-Success "Frontend files copied"
} else {
Write-Warning "Frontend source not found: $sourceDataEntryFrontend"
}
# Create virtual environment
Write-Step "Creating Python virtual environment..."
$venvPath = Join-Path $Config.DataEntryBackendPath "venv"
& python -m venv $venvPath
Write-Success "Virtual environment created"
# Define paths
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
$requirementsPath = Join-Path $Config.DataEntryBackendPath "requirements.txt"
# Install dependencies
Write-Step "Installing Python dependencies..."
if (Test-Path $requirementsPath) {
Write-Info "Upgrading pip..."
& $pythonPath -m pip install --upgrade pip | Out-Default
Write-Info "Installing dependencies..."
& $pipPath install -r $requirementsPath | Out-Default
Write-Success "Python dependencies installed"
}
# Create Windows Service
Write-Step "Creating Windows Service for Data Entry..."
# Remove existing service if present
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$nssmOutput = & nssm status $Config.DataEntryServiceName 2>&1
$serviceExists = $LASTEXITCODE -eq 0
$ErrorActionPreference = $oldErrorAction
if ($serviceExists) {
Write-Info "Removing existing service..."
& nssm stop $Config.DataEntryServiceName 2>&1 | Out-Null
Start-Sleep -Seconds 2
& nssm remove $Config.DataEntryServiceName confirm 2>&1 | Out-Null
Start-Sleep -Seconds 2
}
# Create service (using 1 worker to avoid Windows socket sharing issues with multiple workers)
& nssm install $Config.DataEntryServiceName $pythonPath "-m" "uvicorn" "app.main:app" "--host" "127.0.0.1" "--port" $Config.DataEntryPort.ToString() "--workers" "1"
& nssm set $Config.DataEntryServiceName DisplayName $Config.DataEntryServiceDisplayName
& nssm set $Config.DataEntryServiceName Description $Config.DataEntryServiceDescription
& nssm set $Config.DataEntryServiceName Start SERVICE_AUTO_START
& nssm set $Config.DataEntryServiceName AppDirectory $Config.DataEntryBackendPath
# Set PYTHONPATH to include shared modules
$sharedPath = Join-Path $Config.InstallPath "shared"
& nssm set $Config.DataEntryServiceName AppEnvironmentExtra "PYTHONPATH=$sharedPath"
# Set logging
$stdoutLog = Join-Path $Config.DataEntryLogsPath "stdout.log"
$stderrLog = Join-Path $Config.DataEntryLogsPath "stderr.log"
& nssm set $Config.DataEntryServiceName AppStdout $stdoutLog
& nssm set $Config.DataEntryServiceName AppStderr $stderrLog
& nssm set $Config.DataEntryServiceName AppStdoutCreationDisposition 4
& nssm set $Config.DataEntryServiceName AppStderrCreationDisposition 4
& nssm set $Config.DataEntryServiceName AppExit Default Restart
& nssm set $Config.DataEntryServiceName AppRestartDelay 5000
Write-Success "Windows Service created: $($Config.DataEntryServiceName)"
# Configure IIS for frontend
if (Install-IISModules) {
Write-Step "Configuring IIS for Data Entry frontend..."
Import-Module WebAdministration -ErrorAction Stop
# Create Application Pool
if (Test-Path "IIS:\AppPools\$($Config.DataEntryAppPoolName)") {
Remove-WebAppPool -Name $Config.DataEntryAppPoolName -ErrorAction SilentlyContinue
}
New-WebAppPool -Name $Config.DataEntryAppPoolName -Force | Out-Null
Set-ItemProperty -Path "IIS:\AppPools\$($Config.DataEntryAppPoolName)" -Name "managedRuntimeVersion" -Value ""
Write-Success "Application Pool created: $($Config.DataEntryAppPoolName)"
# Create/update application
$existingApp = Get-WebApplication -Name $Config.DataEntryIISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
if ($existingApp) {
Remove-WebApplication -Name $Config.DataEntryIISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
}
New-WebApplication -Name $Config.DataEntryIISAppName `
-Site $Config.IISSiteName `
-PhysicalPath $Config.DataEntryFrontendPath `
-ApplicationPool $Config.DataEntryAppPoolName `
-Force | Out-Null
Write-Success "IIS Application created: /$($Config.DataEntryIISAppName)"
# Copy web.config for data-entry frontend
$webConfigSource = Join-Path (Split-Path $Config.SourcePath -Parent) "config\web.config.data-entry"
$webConfigDest = Join-Path $Config.DataEntryFrontendPath "web.config"
if (Test-Path $webConfigSource) {
Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force
Write-Success "web.config copied for Data Entry frontend"
}
} else {
Write-Warning "IIS modules not installed, skipping IIS configuration"
}
Write-Host "`n" + ("=" * 70) -ForegroundColor Green
Write-Host " DATA ENTRY INSTALLATION COMPLETED" -ForegroundColor Green
Write-Host ("=" * 70) -ForegroundColor Green
Write-Host "`nIMPORTANT: Configure .env file before starting service" -ForegroundColor Yellow
Write-Host "Location: $($Config.DataEntryBackendPath)\.env" -ForegroundColor Yellow
Write-Host "Copy .env.prod to .env and edit with your settings" -ForegroundColor Yellow
return $true
} catch {
Write-Host "`n[INSTALLATION FAILED] $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
return $false
}
}
# =============================================================================
# MENU FUNCTIONS
# =============================================================================
@@ -872,10 +1168,10 @@ function Show-MainMenu {
Write-Host " Main Menu:" -ForegroundColor Yellow
Write-Host ""
Write-Host " [1] Deploy Components" -ForegroundColor White
Write-Host " (Update application files and configurations)" -ForegroundColor Gray
Write-Host " (Update Reports, Telegram Bot, Data Entry)" -ForegroundColor Gray
Write-Host ""
Write-Host " [2] Manage Services" -ForegroundColor White
Write-Host " (Start, stop, restart Backend and Telegram Bot)" -ForegroundColor Gray
Write-Host " (Start, stop, restart all services)" -ForegroundColor Gray
Write-Host ""
Write-Host " [3] Check Status" -ForegroundColor White
Write-Host " (View service status and health checks)" -ForegroundColor Gray
@@ -908,14 +1204,20 @@ function Show-DeployMenu {
Write-Host ""
Write-Host " Select what to deploy:" -ForegroundColor Yellow
Write-Host ""
Write-Host " [1] Backend + Frontend" -ForegroundColor White
Write-Host " --- Reports App ---" -ForegroundColor Cyan
Write-Host " [1] Reports Backend + Frontend" -ForegroundColor White
Write-Host " (FastAPI backend + Vue.js frontend files)" -ForegroundColor Gray
Write-Host ""
Write-Host " [2] Telegram Bot" -ForegroundColor White
Write-Host " (Telegram bot application only)" -ForegroundColor Gray
Write-Host ""
Write-Host " [3] All Components" -ForegroundColor White
Write-Host " (Backend + Frontend + Telegram Bot)" -ForegroundColor Gray
Write-Host " --- Data Entry App ---" -ForegroundColor Cyan
Write-Host " [3] Data Entry App" -ForegroundColor White
Write-Host " (Data Entry backend + frontend)" -ForegroundColor Gray
Write-Host ""
Write-Host " --- Combined ---" -ForegroundColor Cyan
Write-Host " [4] All Components" -ForegroundColor White
Write-Host " (Reports + Telegram Bot + Data Entry)" -ForegroundColor Gray
Write-Host ""
Write-Host " [B] Back to Main Menu" -ForegroundColor Yellow
Write-Host ""
@@ -928,10 +1230,11 @@ function Show-DeployMenu {
switch ($choice.ToUpper()) {
"1" { return "Backend" }
"2" { return "TelegramBot" }
"3" { return "All" }
"3" { return "DataEntry" }
"4" { return "All" }
"B" { return "Back" }
default {
Write-Host "Invalid choice. Please select 1-3 or B." -ForegroundColor Red
Write-Host "Invalid choice. Please select 1-4 or B." -ForegroundColor Red
}
}
} while ($true)
@@ -948,16 +1251,21 @@ function Show-ManageMenu {
Write-Host " [2] Stop All Services" -ForegroundColor Red
Write-Host " [3] Restart All Services" -ForegroundColor Yellow
Write-Host ""
Write-Host " Backend Service:" -ForegroundColor Yellow
Write-Host " [4] Start Backend" -ForegroundColor Green
Write-Host " [5] Stop Backend" -ForegroundColor Red
Write-Host " [6] Restart Backend" -ForegroundColor Yellow
Write-Host " Reports Backend Service:" -ForegroundColor Yellow
Write-Host " [4] Start Reports Backend" -ForegroundColor Green
Write-Host " [5] Stop Reports Backend" -ForegroundColor Red
Write-Host " [6] Restart Reports Backend" -ForegroundColor Yellow
Write-Host ""
Write-Host " Telegram Bot Service:" -ForegroundColor Yellow
Write-Host " [7] Start Telegram Bot" -ForegroundColor Green
Write-Host " [8] Stop Telegram Bot" -ForegroundColor Red
Write-Host " [9] Restart Telegram Bot" -ForegroundColor Yellow
Write-Host ""
Write-Host " Data Entry Service:" -ForegroundColor Yellow
Write-Host " [A] Start Data Entry" -ForegroundColor Green
Write-Host " [S] Stop Data Entry" -ForegroundColor Red
Write-Host " [D] Restart Data Entry" -ForegroundColor Yellow
Write-Host ""
Write-Host " [B] Back to Main Menu" -ForegroundColor Gray
Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan
@@ -976,9 +1284,12 @@ function Show-ManageMenu {
"7" { return @{ Action = "Start"; Component = "TelegramBot" } }
"8" { return @{ Action = "Stop"; Component = "TelegramBot" } }
"9" { return @{ Action = "Restart"; Component = "TelegramBot" } }
"A" { return @{ Action = "Start"; Component = "DataEntry" } }
"S" { return @{ Action = "Stop"; Component = "DataEntry" } }
"D" { return @{ Action = "Restart"; Component = "DataEntry" } }
"B" { return @{ Action = "Back"; Component = "" } }
default {
Write-Host "Invalid choice. Please select 1-9 or B." -ForegroundColor Red
Write-Host "Invalid choice. Please select 1-9, A, S, D, or B." -ForegroundColor Red
}
}
} while ($true)
@@ -1496,6 +1807,18 @@ function Deploy-Backend {
# Update files
Update-BackendFiles
# Update shared modules (always update to ensure all subfolders are present)
$sourceShared = Join-Path $Config.SourcePath "shared"
if (Test-Path $sourceShared) {
$destShared = Join-Path $Config.InstallPath "shared"
if (-not (Test-Path $destShared)) {
New-Item -ItemType Directory -Path $destShared -Force | Out-Null
}
Copy-Item -Path "$sourceShared\*" -Destination $destShared -Recurse -Force -Exclude @("__pycache__", "*.pyc")
Write-Success "Shared modules updated"
}
Update-FrontendFiles
# Start backend service
@@ -1558,10 +1881,12 @@ function Deploy-TelegramBot {
$originalSourcePath = $Config.SourcePath
$Config.SourcePath = Join-Path $originalSourcePath "telegram-bot"
Update-TelegramBotFiles
# Restore original source path
$Config.SourcePath = $originalSourcePath
try {
Update-TelegramBotFiles
} finally {
# ALWAYS restore original source path (even on error)
$Config.SourcePath = $originalSourcePath
}
# Start telegram bot service
$started = Start-ServiceComponent -ComponentName "Telegram Bot" `
@@ -1585,6 +1910,297 @@ function Deploy-TelegramBot {
}
}
function Update-DataEntryBackendFiles {
Write-Step "Updating Data Entry backend files..."
$sourceDataEntryBackend = Join-Path $Config.SourcePath "data-entry-backend"
if (-not (Test-Path $sourceDataEntryBackend)) {
throw "Source data-entry-backend path not found: $sourceDataEntryBackend"
}
try {
# Copy app directory
$sourceApp = Join-Path $sourceDataEntryBackend "app"
$destApp = Join-Path $Config.DataEntryBackendPath "app"
if (Test-Path $destApp) {
Remove-Item -Path $destApp -Recurse -Force
Write-Success "Removed old app directory"
}
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
Write-Success "Application files updated"
# Copy migrations directory
$sourceMigrations = Join-Path $sourceDataEntryBackend "migrations"
$destMigrations = Join-Path $Config.DataEntryBackendPath "migrations"
if (Test-Path $sourceMigrations) {
if (Test-Path $destMigrations) {
Remove-Item -Path $destMigrations -Recurse -Force
}
Copy-Item -Path $sourceMigrations -Destination $destMigrations -Recurse -Force
Write-Success "Migrations updated"
}
# Copy alembic.ini
$sourceAlembic = Join-Path $sourceDataEntryBackend "alembic.ini"
if (Test-Path $sourceAlembic) {
Copy-Item -Path $sourceAlembic -Destination $Config.DataEntryBackendPath -Force
Write-Success "alembic.ini updated"
}
# Update requirements.txt if changed
$sourceReq = Join-Path $sourceDataEntryBackend "requirements.txt"
$destReq = Join-Path $Config.DataEntryBackendPath "requirements.txt"
if (Test-Path $sourceReq) {
$sourceHash = (Get-FileHash $sourceReq -Algorithm SHA256).Hash
$destHash = if (Test-Path $destReq) {
(Get-FileHash $destReq -Algorithm SHA256).Hash
} else {
""
}
if ($sourceHash -ne $destHash) {
Write-Step "Requirements changed, updating Python dependencies..."
Copy-Item -Path $sourceReq -Destination $destReq -Force
# Use virtual environment pip
$venvPath = Join-Path $Config.DataEntryBackendPath "venv"
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
if (Test-Path $pipPath) {
& $pipPath install -r $destReq --upgrade
Write-Success "Python dependencies updated"
} else {
Write-Warning "Virtual environment not found, skipping dependency update"
}
} else {
Write-Success "Python dependencies unchanged"
}
}
# Copy .env templates (always update to keep in sync)
foreach ($envFile in @(".env.example", ".env.prod", ".env.test")) {
$sourceEnv = Join-Path $sourceDataEntryBackend $envFile
$destEnv = Join-Path $Config.DataEntryBackendPath $envFile
if (Test-Path $sourceEnv) {
Copy-Item -Path $sourceEnv -Destination $destEnv -Force
Write-Success "$envFile template updated"
}
}
# Preserve .env file (or create from .env.prod if missing)
$envFile = Join-Path $Config.DataEntryBackendPath ".env"
if (-not (Test-Path $envFile)) {
$sourceEnvProd = Join-Path $sourceDataEntryBackend ".env.prod"
if (Test-Path $sourceEnvProd) {
Copy-Item -Path $sourceEnvProd -Destination $envFile -Force
Write-Warning "Created .env from .env.prod - PLEASE CONFIGURE"
}
} else {
Write-Success ".env file preserved"
}
# Run database migrations
Write-Step "Running database migrations..."
$venvPath = Join-Path $Config.DataEntryBackendPath "venv"
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
$alembicPath = Join-Path $venvPath "Scripts\alembic.exe"
if (Test-Path $alembicPath) {
Push-Location $Config.DataEntryBackendPath
try {
& $alembicPath upgrade head
if ($LASTEXITCODE -eq 0) {
Write-Success "Database migrations completed"
} else {
Write-Warning "Migration returned non-zero exit code"
}
} finally {
Pop-Location
}
} else {
Write-Warning "Alembic not found, skipping migrations"
}
} catch {
Write-Error "Failed to update Data Entry backend files: $_"
throw
}
}
function Update-DataEntryFrontendFiles {
Write-Step "Updating Data Entry frontend files..."
$sourceDataEntryFrontend = Join-Path $Config.SourcePath "data-entry-frontend"
if (-not (Test-Path $sourceDataEntryFrontend)) {
throw "Source data-entry-frontend path not found: $sourceDataEntryFrontend"
}
try {
# Remove old frontend files (except web.config)
if (Test-Path $Config.DataEntryFrontendPath) {
Get-ChildItem -Path $Config.DataEntryFrontendPath -Exclude "web.config" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
# Copy new frontend files
Copy-Item -Path "$sourceDataEntryFrontend\*" -Destination $Config.DataEntryFrontendPath -Recurse -Force
# Ensure web.config exists (copy from config if missing)
$webConfigDest = Join-Path $Config.DataEntryFrontendPath "web.config"
if (-not (Test-Path $webConfigDest)) {
$webConfigSource = Join-Path (Split-Path $Config.SourcePath -Parent) "config\web.config.data-entry"
if (Test-Path $webConfigSource) {
Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force
Write-Success "web.config copied for Data Entry frontend (was missing)"
} else {
Write-Warning "web.config.data-entry not found in config folder"
}
}
Write-Success "Data Entry frontend files updated"
} catch {
Write-Error "Failed to update Data Entry frontend files: $_"
throw
}
}
function Backup-DataEntryDeployment {
Write-Step "Backing up Data Entry deployment..."
New-BackupDirectory
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupName = "backup-DataEntry-$timestamp"
$backupFullPath = Join-Path $Config.BackupPath $backupName
try {
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
# Backup app directory
$appPath = Join-Path $Config.DataEntryBackendPath "app"
if (Test-Path $appPath) {
$backupAppPath = Join-Path $backupFullPath "app"
Copy-Item -Path $appPath -Destination $backupAppPath -Recurse -Force
Write-Success "App files backed up"
}
# Backup migrations
$migrationsPath = Join-Path $Config.DataEntryBackendPath "migrations"
if (Test-Path $migrationsPath) {
$backupMigrationsPath = Join-Path $backupFullPath "migrations"
Copy-Item -Path $migrationsPath -Destination $backupMigrationsPath -Recurse -Force
Write-Success "Migrations backed up"
}
# Backup requirements.txt
$reqFile = Join-Path $Config.DataEntryBackendPath "requirements.txt"
if (Test-Path $reqFile) {
Copy-Item -Path $reqFile -Destination (Join-Path $backupFullPath "requirements.txt") -Force
Write-Success "Requirements file backed up"
}
# Backup .env
$envFile = Join-Path $Config.DataEntryBackendPath ".env"
if (Test-Path $envFile) {
Copy-Item -Path $envFile -Destination (Join-Path $backupFullPath ".env") -Force
Write-Success ".env file backed up"
}
# Backup database
$dbFile = Join-Path $Config.DataEntryBackendPath "data\receipts*.db"
$dbFiles = Get-ChildItem -Path (Join-Path $Config.DataEntryBackendPath "data") -Filter "*.db" -ErrorAction SilentlyContinue
if ($dbFiles) {
$backupDataPath = Join-Path $backupFullPath "data"
New-Item -ItemType Directory -Path $backupDataPath -Force | Out-Null
foreach ($db in $dbFiles) {
Copy-Item -Path $db.FullName -Destination (Join-Path $backupDataPath $db.Name) -Force
}
Write-Success "Database(s) backed up"
}
Write-Success "Backup created at: $backupFullPath"
return $backupFullPath
} catch {
Write-Error "Backup failed: $_"
throw
}
}
function Deploy-DataEntry {
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " DEPLOYING DATA ENTRY APP" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
try {
# Auto-detect: First-time install or update?
$isInstalled = Test-DataEntryInstalled
if (-not $isInstalled) {
Write-Host ""
Write-Warning "Data Entry NOT detected - performing FIRST-TIME INSTALLATION"
Write-Host ""
Start-Sleep -Seconds 2
# Route to first-time installation
return Install-DataEntryFirstTime
}
# Data Entry is already installed - proceed with UPDATE
Write-Host ""
Write-Info "Data Entry detected - performing UPDATE"
Write-Host ""
# Create backup
$backupPath = Backup-DataEntryDeployment
# Stop data entry service
$stopped = Stop-ServiceComponent -ComponentName "Data Entry" -ServiceName $Config.DataEntryServiceName
if (-not $stopped) {
throw "Failed to stop Data Entry service"
}
# Update files
Update-DataEntryBackendFiles
# Update shared modules (always update to ensure all subfolders are present)
$sourceShared = Join-Path $Config.SourcePath "shared"
if (Test-Path $sourceShared) {
$destShared = Join-Path $Config.InstallPath "shared"
if (-not (Test-Path $destShared)) {
New-Item -ItemType Directory -Path $destShared -Force | Out-Null
}
Copy-Item -Path "$sourceShared\*" -Destination $destShared -Recurse -Force -Exclude @("__pycache__", "*.pyc")
Write-Success "Shared modules updated"
}
Update-DataEntryFrontendFiles
# Start data entry service
$started = Start-ServiceComponent -ComponentName "Data Entry" `
-ServiceName $Config.DataEntryServiceName `
-HealthUrl $Config.DataEntryHealthUrl `
-HealthTimeout $Config.DataEntryHealthTimeout
if ($started) {
Write-Host "`n" + ("=" * 70) -ForegroundColor Green
Write-Host " DATA ENTRY APP DEPLOYED SUCCESSFULLY" -ForegroundColor Green
Write-Host ("=" * 70) -ForegroundColor Green
Write-Host " Backup: $backupPath" -ForegroundColor Gray
return $true
} else {
throw "Failed to start Data Entry service after deployment"
}
} catch {
Write-Host "`n[DEPLOYMENT FAILED] $_" -ForegroundColor Red
return $false
}
}
# =============================================================================
# MAIN EXECUTION FLOW
# =============================================================================
@@ -1601,15 +2217,17 @@ function Execute-ManageAction {
switch ($Component) {
"All" {
$components = @(
@{ Name = "Backend"; ServiceName = $Config.BackendServiceName;
@{ Name = "Reports Backend"; ServiceName = $Config.BackendServiceName;
HealthUrl = $Config.BackendHealthUrl; HealthTimeout = $Config.BackendHealthTimeout },
@{ Name = "Telegram Bot"; ServiceName = $Config.TelegramBotServiceName;
HealthUrl = $Config.TelegramBotHealthUrl; HealthTimeout = $Config.TelegramBotHealthTimeout }
HealthUrl = $Config.TelegramBotHealthUrl; HealthTimeout = $Config.TelegramBotHealthTimeout },
@{ Name = "Data Entry"; ServiceName = $Config.DataEntryServiceName;
HealthUrl = $Config.DataEntryHealthUrl; HealthTimeout = $Config.DataEntryHealthTimeout }
)
}
"Backend" {
$components = @(
@{ Name = "Backend"; ServiceName = $Config.BackendServiceName;
@{ Name = "Reports Backend"; ServiceName = $Config.BackendServiceName;
HealthUrl = $Config.BackendHealthUrl; HealthTimeout = $Config.BackendHealthTimeout }
)
}
@@ -1619,6 +2237,12 @@ function Execute-ManageAction {
HealthUrl = $Config.TelegramBotHealthUrl; HealthTimeout = $Config.TelegramBotHealthTimeout }
)
}
"DataEntry" {
$components = @(
@{ Name = "Data Entry"; ServiceName = $Config.DataEntryServiceName;
HealthUrl = $Config.DataEntryHealthUrl; HealthTimeout = $Config.DataEntryHealthTimeout }
)
}
}
foreach ($comp in $components) {
@@ -1665,11 +2289,16 @@ function Execute-DeployAction {
$result = Deploy-TelegramBot
$result # Explicitly output the boolean
}
"DataEntry" {
$result = Deploy-DataEntry
$result # Explicitly output the boolean
}
"All" {
$backendOk = Deploy-Backend
$telegramOk = Deploy-TelegramBot
$dataEntryOk = Deploy-DataEntry
# Return combined result
($backendOk -and $telegramOk)
($backendOk -and $telegramOk -and $dataEntryOk)
}
}
@@ -1695,7 +2324,7 @@ function Show-AllStatus {
Write-Host " ROA2WEB - System Status" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan
Show-ServiceStatus -ComponentName "Backend" `
Show-ServiceStatus -ComponentName "Reports Backend" `
-ServiceName $Config.BackendServiceName `
-HealthUrl $Config.BackendHealthUrl `
-HealthTimeout $Config.BackendHealthTimeout
@@ -1705,6 +2334,11 @@ function Show-AllStatus {
-HealthUrl $Config.TelegramBotHealthUrl `
-HealthTimeout $Config.TelegramBotHealthTimeout
Show-ServiceStatus -ComponentName "Data Entry" `
-ServiceName $Config.DataEntryServiceName `
-HealthUrl $Config.DataEntryHealthUrl `
-HealthTimeout $Config.DataEntryHealthTimeout
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
}
@@ -1722,10 +2356,14 @@ function Main {
$success = switch ($Action) {
"DeployBackend" { Execute-DeployAction -Component "Backend" }
"DeployTelegramBot" { Execute-DeployAction -Component "TelegramBot" }
"DeployDataEntry" { Execute-DeployAction -Component "DataEntry" }
"DeployAll" { Execute-DeployAction -Component "All" }
"StartAll" { Execute-ManageAction -Action "Start" -Component "All"; $true }
"StopAll" { Execute-ManageAction -Action "Stop" -Component "All"; $true }
"RestartAll" { Execute-ManageAction -Action "Restart" -Component "All"; $true }
"StartDataEntry" { Execute-ManageAction -Action "Start" -Component "DataEntry"; $true }
"StopDataEntry" { Execute-ManageAction -Action "Stop" -Component "DataEntry"; $true }
"RestartDataEntry" { Execute-ManageAction -Action "Restart" -Component "DataEntry"; $true }
"Status" { Show-AllStatus; $true }
default { $false }
}