From 3295f60faaded4910349ada6f6e0e0087bb4a41a Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Thu, 18 Dec 2025 19:44:15 +0200 Subject: [PATCH] feat: Improve Windows deployment and fix production paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 9 + data-entry-app/backend/app/main.py | 22 +- data-entry-app/backend/requirements.txt | 2 + data-entry-app/frontend/src/router/index.js | 2 +- data-entry-app/frontend/vite.config.js | 2 + deployment/windows/scripts/Build-ROA2WEB.ps1 | 288 +++++++- .../windows/scripts/Check-And-Deploy.ps1 | 1 + .../windows/scripts/Publish-And-Deploy.ps1 | 33 +- .../windows/scripts/ROA2WEB-Console.ps1 | 690 +++++++++++++++++- deployment/windows/scripts/deploy-config.json | 28 + 10 files changed, 1015 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 8f27094..f90a944 100644 --- a/.gitignore +++ b/.gitignore @@ -356,8 +356,17 @@ deploy-package/ # Build cache (npm node_modules cache for faster builds) deployment/windows/.build-cache/ +deployment/windows/.build-cache-*/ .build-cache/ +.build-cache-*/ **/.build-cache/ +**/.build-cache-*/ + +# Shared folder copied during build (temporary) +deployment/shared/ + +# Temp frontend build directories +**/.temp-frontend-build/ # Deployment logs and temporary files deployment/windows/scripts/*.log diff --git a/data-entry-app/backend/app/main.py b/data-entry-app/backend/app/main.py index 978e83e..19191e1 100644 --- a/data-entry-app/backend/app/main.py +++ b/data-entry-app/backend/app/main.py @@ -22,8 +22,26 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles # Add shared modules to path -project_root = Path(__file__).parent.parent.parent.parent -sys.path.insert(0, str(project_root / "shared")) +# Development: data-entry-app/backend/app/main.py -> 4 parents to project root +# Production: data-entry-backend/app/main.py -> 3 parents to roa2web root +def find_shared_path(): + """Find shared folder - works in both dev and production.""" + current = Path(__file__).parent # app/ + + # Try different parent levels to find shared folder + for levels in range(1, 6): + candidate = current + for _ in range(levels): + candidate = candidate.parent + shared_path = candidate / "shared" + if shared_path.exists() and (shared_path / "auth").exists(): + return shared_path + + # Fallback to original logic + return Path(__file__).parent.parent.parent.parent / "shared" + +shared_path = find_shared_path() +sys.path.insert(0, str(shared_path)) from app.config import settings from app.db.database import init_db diff --git a/data-entry-app/backend/requirements.txt b/data-entry-app/backend/requirements.txt index 08770f4..e201548 100644 --- a/data-entry-app/backend/requirements.txt +++ b/data-entry-app/backend/requirements.txt @@ -11,6 +11,7 @@ alembic>=1.13.1 # Pydantic pydantic>=2.5.3 pydantic-settings>=2.1.0 +email-validator>=2.1.0 # File handling python-multipart>=0.0.6 @@ -19,6 +20,7 @@ Pillow>=10.2.0 # Authentication (shared) PyJWT>=2.8.0 +python-jose[cryptography]>=3.3.0 # Oracle (for nomenclatures) oracledb>=2.0.1 diff --git a/data-entry-app/frontend/src/router/index.js b/data-entry-app/frontend/src/router/index.js index f093470..78b3323 100644 --- a/data-entry-app/frontend/src/router/index.js +++ b/data-entry-app/frontend/src/router/index.js @@ -35,7 +35,7 @@ const routes = [ ] const router = createRouter({ - history: createWebHistory(), + history: createWebHistory(import.meta.env.BASE_URL), routes }) diff --git a/data-entry-app/frontend/vite.config.js b/data-entry-app/frontend/vite.config.js index 27adea9..f2ef414 100644 --- a/data-entry-app/frontend/vite.config.js +++ b/data-entry-app/frontend/vite.config.js @@ -4,6 +4,8 @@ import { fileURLToPath, URL } from 'node:url' export default defineConfig({ plugins: [vue()], + // Base path for production deployment in IIS subdirectory + base: process.env.NODE_ENV === 'production' ? '/data-entry/' : '/', resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), diff --git a/deployment/windows/scripts/Build-ROA2WEB.ps1 b/deployment/windows/scripts/Build-ROA2WEB.ps1 index 3852065..7a9d3d1 100644 --- a/deployment/windows/scripts/Build-ROA2WEB.ps1 +++ b/deployment/windows/scripts/Build-ROA2WEB.ps1 @@ -68,7 +68,7 @@ [CmdletBinding()] param( - [ValidateSet("All", "Frontend", "Backend", "TelegramBot")] + [ValidateSet("All", "Frontend", "Backend", "TelegramBot", "DataEntryApp", "DataEntryBackend")] [string]$Component = "", [string]$OutputPath = "./deploy-package", @@ -85,9 +85,14 @@ $ErrorActionPreference = "Stop" # ============================================================================= $config = @{ + # Reports App sources BackendSource = "../../reports-app/backend" FrontendSource = "../../reports-app/frontend" TelegramBotSource = "../../reports-app/telegram-bot" + # Data Entry App sources + DataEntryBackendSource = "../../data-entry-app/backend" + DataEntryFrontendSource = "../../data-entry-app/frontend" + # Shared sources SharedSource = "../../shared" ConfigSource = "../config" RequiredNodeVersion = 16 @@ -175,17 +180,25 @@ function Show-BuildMenu { Write-Host " Select components to build:" -ForegroundColor Yellow Write-Host "" Write-Host " [1] All Components" -ForegroundColor White - Write-Host " (Frontend + Backend + Telegram Bot)" -ForegroundColor Gray + Write-Host " (Reports App + Telegram Bot + Data Entry App)" -ForegroundColor Gray Write-Host "" - Write-Host " [2] Frontend + Backend" -ForegroundColor White + Write-Host " --- Reports App ---" -ForegroundColor Cyan + Write-Host " [2] Reports Frontend + Backend" -ForegroundColor White Write-Host " (Vue.js build + FastAPI backend files)" -ForegroundColor Gray Write-Host "" - Write-Host " [3] Backend Only" -ForegroundColor White + Write-Host " [3] Reports Backend Only" -ForegroundColor White Write-Host " (FastAPI backend files + shared modules)" -ForegroundColor Gray Write-Host "" Write-Host " [4] Telegram Bot Only" -ForegroundColor White Write-Host " (Telegram bot standalone package)" -ForegroundColor Gray Write-Host "" + Write-Host " --- Data Entry App ---" -ForegroundColor Cyan + Write-Host " [5] Data Entry Frontend + Backend" -ForegroundColor White + Write-Host " (Vue.js build + FastAPI backend files)" -ForegroundColor Gray + Write-Host "" + Write-Host " [6] Data Entry Backend Only" -ForegroundColor White + Write-Host " (FastAPI backend files only)" -ForegroundColor Gray + Write-Host "" Write-Host " [C] Clean Build Cache" -ForegroundColor Yellow Write-Host " (Remove cached node_modules to free disk space)" -ForegroundColor Gray Write-Host "" @@ -202,6 +215,8 @@ function Show-BuildMenu { "2" { return "Frontend" } "3" { return "Backend" } "4" { return "TelegramBot" } + "5" { return "DataEntryApp" } + "6" { return "DataEntryBackend" } "C" { Clear-BuildCache Write-Host "`nPress any key to return to menu..." -ForegroundColor Gray @@ -213,7 +228,7 @@ function Show-BuildMenu { exit 0 } default { - Write-Host "Invalid choice. Please select 1-4, C or Q." -ForegroundColor Red + Write-Host "Invalid choice. Please select 1-6, C or Q." -ForegroundColor Red } } } while ($true) @@ -258,18 +273,28 @@ function Build-Frontend { throw "Frontend source path not found: $SourcePath" } - # Create temporary build directory - $tempBuildDir = Join-Path $OutputPath ".temp-frontend-build" - if (Test-Path $tempBuildDir) { + # Determine if this is reports-app or data-entry-app based on source path + $appFolder = Split-Path (Split-Path $SourcePath -Parent) -Leaf # "reports-app" or "data-entry-app" + + # Create temporary build directory with full project structure + # Structure: + # .temp-frontend-build/ + # ├── reports-app/frontend/ (or data-entry-app/frontend/) + # └── shared/ + $tempRootDir = Join-Path $OutputPath ".temp-frontend-build" + if (Test-Path $tempRootDir) { Write-Step "Cleaning existing temp build directory..." - Remove-Item -Path $tempBuildDir -Recurse -Force + Remove-Item -Path $tempRootDir -Recurse -Force } + + # Create the app-specific path inside temp + $tempBuildDir = Join-Path $tempRootDir "$appFolder\frontend" New-Item -ItemType Directory -Path $tempBuildDir -Force | Out-Null Write-Success "Created temp build directory (isolated from WSL)" # Create cache directory for node_modules (OUTSIDE deploy-package) $scriptDir = Split-Path -Parent $PSScriptRoot - $cacheDir = Join-Path $scriptDir ".build-cache" + $cacheDir = Join-Path $scriptDir ".build-cache-$appFolder" if (-not (Test-Path $cacheDir)) { New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null } @@ -318,7 +343,30 @@ function Build-Frontend { } Write-Success "Frontend sources copied to temp" - # Build in temp directory + # Copy shared folder to maintain relative imports (../../../shared/frontend/) + # Now shared goes into tempRootDir/shared (same level as reports-app or data-entry-app) + $projectRoot = Split-Path (Split-Path $SourcePath -Parent) -Parent + $sharedSourcePath = Join-Path $projectRoot "shared" + + if (Test-Path $sharedSourcePath) { + $sharedDestPath = Join-Path $tempRootDir "shared" + + Write-Step "Copying shared components for build..." + Write-Info "Source: $sharedSourcePath" + Write-Info "Dest: $sharedDestPath" + + # Remove existing shared folder in dest if exists + if (Test-Path $sharedDestPath) { + Remove-Item -Path $sharedDestPath -Recurse -Force + } + + Copy-Item -Path $sharedSourcePath -Destination $sharedDestPath -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests") + Write-Success "Shared components copied for relative imports" + } else { + Write-Warning "Shared folder not found at: $sharedSourcePath" + } + + # Build in temp directory (now at tempRootDir/appFolder/frontend) Push-Location $tempBuildDir try { # Check if dependencies need to be reinstalled @@ -386,6 +434,24 @@ function Build-Frontend { throw "Vite not found in node_modules - devDependencies not installed" } + # Create junction for node_modules at tempRootDir level + # This allows shared folder to resolve npm dependencies + $tempRootNodeModules = Join-Path $tempRootDir "node_modules" + if (-not (Test-Path $tempRootNodeModules)) { + Write-Step "Creating node_modules junction for shared imports..." + # Use cmd /c mklink /J for directory junction (works without admin rights) + $junctionResult = cmd /c mklink /J "$tempRootNodeModules" "$nodeModulesPath" 2>&1 + if (Test-Path $tempRootNodeModules) { + Write-Success "Node modules junction created for shared folder access" + } else { + Write-Warning "Could not create junction: $junctionResult" + # Fallback: copy node_modules (slower but works) + Write-Info "Falling back to copying node_modules..." + Copy-Item -Path $nodeModulesPath -Destination $tempRootNodeModules -Recurse -Force + Write-Success "Node modules copied to root (fallback)" + } + } + # Build for production Write-Step "Building for production..." $env:NODE_ENV = "production" @@ -596,6 +662,112 @@ function Copy-TelegramBotFiles { } } +function Copy-DataEntryBackendFiles { + param( + [string]$SourcePath, + [string]$DestPath + ) + + Write-Step "Copying Data Entry backend files..." + + if (-not (Test-Path $SourcePath)) { + throw "Data Entry backend source path not found: $SourcePath" + } + + if (-not (Test-Path $DestPath)) { + New-Item -ItemType Directory -Path $DestPath -Force | Out-Null + } + + # Exclude patterns + $excludeDirs = @("venv", "__pycache__", ".pytest_cache", "logs", "temp", "node_modules", "data") + $excludeFiles = @("*.pyc", "*.pyo", "*.log", ".env", ".env.local", "*.db") + + $normalizedSourcePath = $SourcePath.TrimEnd('\', '/') + '\' + + $testExclude = { + param([string]$RelativePath, [bool]$IsDirectory, [array]$ExcludeDirs, [array]$ExcludeFiles) + + if ($IsDirectory) { + $dirName = Split-Path $RelativePath -Leaf + if ($ExcludeDirs -contains $dirName) { + return $true + } + } + + $pathParts = $RelativePath -split '[\\/]' + foreach ($part in $pathParts) { + if ($ExcludeDirs -contains $part) { + return $true + } + } + + if (-not $IsDirectory) { + foreach ($pattern in $ExcludeFiles) { + if ($RelativePath -like $pattern) { + return $true + } + } + } + + return $false + } + + Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object { + if ($_.FullName.Length -le $normalizedSourcePath.Length) { + return + } + $relativePath = $_.FullName.Substring($normalizedSourcePath.Length) + + $shouldExclude = & $testExclude -RelativePath $relativePath -IsDirectory $_.PSIsContainer -ExcludeDirs $excludeDirs -ExcludeFiles $excludeFiles + if ($shouldExclude) { + return + } + + $destFile = Join-Path $DestPath $relativePath + + if ($_.PSIsContainer) { + if (-not (Test-Path $destFile)) { + New-Item -ItemType Directory -Path $destFile -Force | Out-Null + } + } else { + $destDir = Split-Path $destFile -Parent + if (-not (Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + Copy-Item -Path $_.FullName -Destination $destFile -Force + } + } + + $backendFiles = Get-ChildItem -Path $DestPath -Recurse -File + Write-Success "Copied $(($backendFiles).Count) Data Entry backend files" + + # Copy .env.example explicitly + $sourceEnvExample = Join-Path $SourcePath ".env.example" + $destEnvExample = Join-Path $DestPath ".env.example" + if (Test-Path $sourceEnvExample) { + Copy-Item -Path $sourceEnvExample -Destination $destEnvExample -Force + Write-Success ".env.example template copied" + } + + # Copy .env.prod and .env.test templates + foreach ($envFile in @(".env.prod", ".env.test")) { + $sourceEnv = Join-Path $SourcePath $envFile + $destEnv = Join-Path $DestPath $envFile + if (Test-Path $sourceEnv) { + Copy-Item -Path $sourceEnv -Destination $destEnv -Force + Write-Success "$envFile template copied" + } + } + + # Verify requirements.txt + $requirementsTxt = Join-Path $DestPath "requirements.txt" + if (-not (Test-Path $requirementsTxt)) { + Write-Error "CRITICAL: requirements.txt not found!" + throw "Data Entry backend package incomplete - missing requirements.txt" + } + Write-Success "Verified: requirements.txt present" +} + function Copy-SharedModules { param( [string]$SourcePath, @@ -726,10 +898,12 @@ function New-DeploymentReadme { Write-Step "Creating deployment README..." $componentDesc = switch ($ComponentType) { - "All" { "COMPLETE DEPLOYMENT PACKAGE (Frontend + Backend + Telegram Bot)" } - "Frontend" { "FRONTEND + BACKEND DEPLOYMENT PACKAGE" } - "Backend" { "BACKEND DEPLOYMENT PACKAGE" } + "All" { "COMPLETE DEPLOYMENT PACKAGE (Reports + Telegram + Data Entry)" } + "Frontend" { "REPORTS FRONTEND + BACKEND DEPLOYMENT PACKAGE" } + "Backend" { "REPORTS BACKEND DEPLOYMENT PACKAGE" } "TelegramBot" { "TELEGRAM BOT DEPLOYMENT PACKAGE" } + "DataEntryApp" { "DATA ENTRY APP DEPLOYMENT PACKAGE (Frontend + Backend)" } + "DataEntryBackend" { "DATA ENTRY BACKEND DEPLOYMENT PACKAGE" } } $readme = @" @@ -743,11 +917,11 @@ CONTENTS: --------- "@ - if ($ComponentType -eq "All" -or $ComponentType -eq "Frontend") { + if ($ComponentType -eq "All" -or $ComponentType -eq "Frontend" -or $ComponentType -eq "Backend") { $readme += @" - backend/ FastAPI backend application (Python) - frontend/ Vue.js static files (production build) + backend/ Reports App - FastAPI backend (Python, port 8000) + frontend/ Reports App - Vue.js static files (production build) shared/ Shared Python modules (auth, database, utils) config/ Configuration templates (.env, web.config) "@ @@ -756,7 +930,20 @@ CONTENTS: if ($ComponentType -eq "All" -or $ComponentType -eq "TelegramBot") { $readme += @" - telegram-bot/ Telegram bot application + telegram-bot/ Telegram bot application (port 8002) +"@ + } + + if ($ComponentType -eq "All" -or $ComponentType -eq "DataEntryApp" -or $ComponentType -eq "DataEntryBackend") { + $readme += @" + + data-entry-backend/ Data Entry App - FastAPI backend (Python, port 8003) +"@ + } + + if ($ComponentType -eq "All" -or $ComponentType -eq "DataEntryApp") { + $readme += @" + data-entry-frontend/ Data Entry App - Vue.js static files (production build) "@ } @@ -922,15 +1109,15 @@ function New-DeploymentPackage { # Build based on component type switch ($ComponentType) { "All" { - # Frontend + # Reports Frontend $frontendDistPath = Build-Frontend -SourcePath $Paths.FrontendSource -OutputPath $OutputPath $frontendDest = Join-Path $OutputPath "frontend" New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null - Write-Step "Copying frontend files..." + Write-Step "Copying Reports frontend files..." Copy-Item -Path "$frontendDistPath\*" -Destination $frontendDest -Recurse -Force - Write-Success "Frontend files copied" + Write-Success "Reports frontend files copied" - # Backend + # Reports Backend $backendDest = Join-Path $OutputPath "backend" Copy-BackendFiles -SourcePath $Paths.BackendSource -DestPath $backendDest @@ -945,6 +1132,18 @@ function New-DeploymentPackage { # Telegram Bot $telegramDest = Join-Path $OutputPath "telegram-bot" Copy-TelegramBotFiles -SourcePath $Paths.TelegramBotSource -DestPath $telegramDest + + # Data Entry Frontend + $dataEntryFrontendDistPath = Build-Frontend -SourcePath $Paths.DataEntryFrontendSource -OutputPath $OutputPath + $dataEntryFrontendDest = Join-Path $OutputPath "data-entry-frontend" + New-Item -ItemType Directory -Path $dataEntryFrontendDest -Force | Out-Null + Write-Step "Copying Data Entry frontend files..." + Copy-Item -Path "$dataEntryFrontendDistPath\*" -Destination $dataEntryFrontendDest -Recurse -Force + Write-Success "Data Entry frontend files copied" + + # Data Entry Backend + $dataEntryBackendDest = Join-Path $OutputPath "data-entry-backend" + Copy-DataEntryBackendFiles -SourcePath $Paths.DataEntryBackendSource -DestPath $dataEntryBackendDest } "Frontend" { @@ -988,10 +1187,46 @@ function New-DeploymentPackage { $telegramDest = Join-Path $OutputPath "telegram-bot" Copy-TelegramBotFiles -SourcePath $Paths.TelegramBotSource -DestPath $telegramDest } + + "DataEntryApp" { + # Data Entry Frontend build + $dataEntryFrontendDistPath = Build-Frontend -SourcePath $Paths.DataEntryFrontendSource -OutputPath $OutputPath + $dataEntryFrontendDest = Join-Path $OutputPath "data-entry-frontend" + New-Item -ItemType Directory -Path $dataEntryFrontendDest -Force | Out-Null + Write-Step "Copying Data Entry frontend files..." + Copy-Item -Path "$dataEntryFrontendDistPath\*" -Destination $dataEntryFrontendDest -Recurse -Force + Write-Success "Data Entry frontend files copied" + + # Data Entry Backend + $dataEntryBackendDest = Join-Path $OutputPath "data-entry-backend" + Copy-DataEntryBackendFiles -SourcePath $Paths.DataEntryBackendSource -DestPath $dataEntryBackendDest + + # Shared modules + $sharedDest = Join-Path $OutputPath "shared" + Copy-SharedModules -SourcePath $Paths.SharedSource -DestPath $sharedDest + + # Config templates + $configDest = Join-Path $OutputPath "config" + Copy-ConfigTemplates -SourcePath $Paths.ConfigSource -DestPath $configDest + } + + "DataEntryBackend" { + # Data Entry Backend only + $dataEntryBackendDest = Join-Path $OutputPath "data-entry-backend" + Copy-DataEntryBackendFiles -SourcePath $Paths.DataEntryBackendSource -DestPath $dataEntryBackendDest + + # Shared modules + $sharedDest = Join-Path $OutputPath "shared" + Copy-SharedModules -SourcePath $Paths.SharedSource -DestPath $sharedDest + + # Config templates + $configDest = Join-Path $OutputPath "config" + Copy-ConfigTemplates -SourcePath $Paths.ConfigSource -DestPath $configDest + } } # Cleanup temporary directories - if ($ComponentType -eq "All" -or $ComponentType -eq "Frontend") { + if ($ComponentType -eq "All" -or $ComponentType -eq "Frontend" -or $ComponentType -eq "DataEntryApp") { Remove-TempDirectories -OutputPath $OutputPath } @@ -1041,9 +1276,14 @@ function Main { try { # Resolve paths $paths = @{ + # Reports App sources BackendSource = Resolve-FullPath -Path $config.BackendSource FrontendSource = Resolve-FullPath -Path $config.FrontendSource TelegramBotSource = Resolve-FullPath -Path $config.TelegramBotSource + # Data Entry App sources + DataEntryBackendSource = Resolve-FullPath -Path $config.DataEntryBackendSource + DataEntryFrontendSource = Resolve-FullPath -Path $config.DataEntryFrontendSource + # Shared sources SharedSource = Resolve-FullPath -Path $config.SharedSource ConfigSource = Resolve-FullPath -Path $config.ConfigSource } @@ -1055,7 +1295,7 @@ function Main { Write-Host " Output Path: $outputFullPath" # Validate Node.js for frontend builds - if ($Component -eq "All" -or $Component -eq "Frontend") { + if ($Component -eq "All" -or $Component -eq "Frontend" -or $Component -eq "DataEntryApp") { Test-NodeJS } diff --git a/deployment/windows/scripts/Check-And-Deploy.ps1 b/deployment/windows/scripts/Check-And-Deploy.ps1 index fe0e58b..d1a54d0 100644 --- a/deployment/windows/scripts/Check-And-Deploy.ps1 +++ b/deployment/windows/scripts/Check-And-Deploy.ps1 @@ -315,6 +315,7 @@ function Invoke-Deployment { Services = @{ backend = "Running" telegramBot = "Running" + dataEntry = "Running" } } diff --git a/deployment/windows/scripts/Publish-And-Deploy.ps1 b/deployment/windows/scripts/Publish-And-Deploy.ps1 index 24b8e28..e54997e 100644 --- a/deployment/windows/scripts/Publish-And-Deploy.ps1 +++ b/deployment/windows/scripts/Publish-And-Deploy.ps1 @@ -21,7 +21,7 @@ - ViewConfig: Display current configuration .PARAMETER Component - Component to build (All, Frontend, Backend, TelegramBot) + Component to build (All, Frontend, Backend, TelegramBot, DataEntryApp, DataEntryBackend) .PARAMETER TransferMethod Transfer method (Auto, WindowsShare, SSH) @@ -41,9 +41,13 @@ .\Publish-And-Deploy.ps1 -NonInteractive -Action Build -Component Frontend -TransferMethod SSH Build frontend and publish via SSH +.EXAMPLE + .\Publish-And-Deploy.ps1 -NonInteractive -Action Build -Component DataEntryApp + Build Data Entry App (frontend + backend) and publish + .NOTES Author: ROA2WEB Team - Version: 1.0 (Interactive Build & Publish) + Version: 1.1 (Added Data Entry App support) Requires: PowerShell 5.1+, SSH key configured for remote access #> @@ -54,7 +58,7 @@ param( [ValidateSet("Build", "TestConnections", "ViewConfig")] [string]$Action = "", - [ValidateSet("All", "Frontend", "Backend", "TelegramBot")] + [ValidateSet("All", "Frontend", "Backend", "TelegramBot", "DataEntryApp", "DataEntryBackend")] [string]$Component = "", [ValidateSet("Auto", "WindowsShare", "SSH")] @@ -295,7 +299,7 @@ function Test-AllConnections { function Invoke-Build { param( [Parameter(Mandatory)] - [ValidateSet("All", "Frontend", "Backend", "TelegramBot")] + [ValidateSet("All", "Frontend", "Backend", "TelegramBot", "DataEntryApp", "DataEntryBackend")] [string]$Component ) @@ -554,7 +558,8 @@ function Edit-Configuration { } "4" { Write-Host "`nEdit Build Settings:" -ForegroundColor Yellow - Write-Host "Default Component (All/Frontend/Backend/TelegramBot) [current: $($script:Config.build.defaultComponent)]: " -NoNewline + Write-Host "Default Component (All/Frontend/Backend/TelegramBot/DataEntryApp/DataEntryBackend)" -ForegroundColor Gray + Write-Host "[current: $($script:Config.build.defaultComponent)]: " -NoNewline $newComp = Read-Host if ($newComp) { $script:Config.build.defaultComponent = $newComp } @@ -658,17 +663,25 @@ function Show-ComponentMenu { Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host "" Write-Host " [1] All Components" -ForegroundColor White - Write-Host " (Frontend + Backend + Telegram Bot)" -ForegroundColor Gray + Write-Host " (Reports + Telegram Bot + Data Entry)" -ForegroundColor Gray Write-Host "" - Write-Host " [2] Frontend + Backend" -ForegroundColor White + Write-Host " --- Reports App ---" -ForegroundColor Cyan + Write-Host " [2] Reports Frontend + Backend" -ForegroundColor White Write-Host " (Vue.js build + FastAPI backend)" -ForegroundColor Gray Write-Host "" - Write-Host " [3] Backend Only" -ForegroundColor White + Write-Host " [3] Reports Backend Only" -ForegroundColor White Write-Host " (FastAPI backend + shared modules)" -ForegroundColor Gray Write-Host "" Write-Host " [4] Telegram Bot Only" -ForegroundColor White Write-Host " (Telegram bot standalone package)" -ForegroundColor Gray Write-Host "" + Write-Host " --- Data Entry App ---" -ForegroundColor Cyan + Write-Host " [5] Data Entry Frontend + Backend" -ForegroundColor White + Write-Host " (Vue.js build + FastAPI backend)" -ForegroundColor Gray + Write-Host "" + Write-Host " [6] Data Entry Backend Only" -ForegroundColor White + Write-Host " (FastAPI backend only)" -ForegroundColor Gray + Write-Host "" Write-Host " [B] Back to Main Menu" -ForegroundColor Yellow Write-Host "" Write-Host ("=" * 70) -ForegroundColor Cyan @@ -682,9 +695,11 @@ function Show-ComponentMenu { "2" { return "Frontend" } "3" { return "Backend" } "4" { return "TelegramBot" } + "5" { return "DataEntryApp" } + "6" { return "DataEntryBackend" } "B" { return "Back" } default { - Write-Host "Invalid choice. Please select 1-4 or B." -ForegroundColor Red + Write-Host "Invalid choice. Please select 1-6 or B." -ForegroundColor Red } } } while ($true) diff --git a/deployment/windows/scripts/ROA2WEB-Console.ps1 b/deployment/windows/scripts/ROA2WEB-Console.ps1 index 342c8e2..0062622 100644 --- a/deployment/windows/scripts/ROA2WEB-Console.ps1 +++ b/deployment/windows/scripts/ROA2WEB-Console.ps1 @@ -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 } } diff --git a/deployment/windows/scripts/deploy-config.json b/deployment/windows/scripts/deploy-config.json index 57aebf3..78fb31c 100644 --- a/deployment/windows/scripts/deploy-config.json +++ b/deployment/windows/scripts/deploy-config.json @@ -20,5 +20,33 @@ "deployment": { "autoDeployEnabled": true, "checkIntervalMinutes": 5 + }, + "paths": { + "installRoot": "C:\\inetpub\\wwwroot\\roa2web", + "reportsBackend": "C:\\inetpub\\wwwroot\\roa2web\\backend", + "reportsFrontend": "C:\\inetpub\\wwwroot\\roa2web\\frontend", + "telegramBot": "C:\\inetpub\\wwwroot\\roa2web\\telegram-bot", + "dataEntryBackend": "C:\\inetpub\\wwwroot\\roa2web\\data-entry-backend", + "dataEntryFrontend": "C:\\inetpub\\wwwroot\\roa2web\\data-entry-frontend", + "shared": "C:\\inetpub\\wwwroot\\roa2web\\shared", + "logs": "C:\\inetpub\\wwwroot\\roa2web\\logs", + "backups": "C:\\inetpub\\wwwroot\\roa2web\\backups" + }, + "services": { + "reportsBackend": { + "name": "ROA2WEB-Backend", + "displayName": "ROA2WEB Reports Backend API", + "port": 8000 + }, + "telegramBot": { + "name": "ROA2WEB-TelegramBot", + "displayName": "ROA2WEB Telegram Bot", + "port": 8002 + }, + "dataEntry": { + "name": "ROA2WEB-DataEntry", + "displayName": "ROA2WEB Data Entry API", + "port": 8003 + } } }