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

9
.gitignore vendored
View File

@@ -356,8 +356,17 @@ deploy-package/
# Build cache (npm node_modules cache for faster builds) # Build cache (npm node_modules cache for faster builds)
deployment/windows/.build-cache/ deployment/windows/.build-cache/
deployment/windows/.build-cache-*/
.build-cache/ .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 logs and temporary files
deployment/windows/scripts/*.log deployment/windows/scripts/*.log

View File

@@ -22,8 +22,26 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
# Add shared modules to path # Add shared modules to path
project_root = Path(__file__).parent.parent.parent.parent # Development: data-entry-app/backend/app/main.py -> 4 parents to project root
sys.path.insert(0, str(project_root / "shared")) # 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.config import settings
from app.db.database import init_db from app.db.database import init_db

View File

@@ -11,6 +11,7 @@ alembic>=1.13.1
# Pydantic # Pydantic
pydantic>=2.5.3 pydantic>=2.5.3
pydantic-settings>=2.1.0 pydantic-settings>=2.1.0
email-validator>=2.1.0
# File handling # File handling
python-multipart>=0.0.6 python-multipart>=0.0.6
@@ -19,6 +20,7 @@ Pillow>=10.2.0
# Authentication (shared) # Authentication (shared)
PyJWT>=2.8.0 PyJWT>=2.8.0
python-jose[cryptography]>=3.3.0
# Oracle (for nomenclatures) # Oracle (for nomenclatures)
oracledb>=2.0.1 oracledb>=2.0.1

View File

@@ -35,7 +35,7 @@ const routes = [
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(import.meta.env.BASE_URL),
routes routes
}) })

View File

@@ -4,6 +4,8 @@ import { fileURLToPath, URL } from 'node:url'
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
// Base path for production deployment in IIS subdirectory
base: process.env.NODE_ENV === 'production' ? '/data-entry/' : '/',
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),

View File

@@ -68,7 +68,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateSet("All", "Frontend", "Backend", "TelegramBot")] [ValidateSet("All", "Frontend", "Backend", "TelegramBot", "DataEntryApp", "DataEntryBackend")]
[string]$Component = "", [string]$Component = "",
[string]$OutputPath = "./deploy-package", [string]$OutputPath = "./deploy-package",
@@ -85,9 +85,14 @@ $ErrorActionPreference = "Stop"
# ============================================================================= # =============================================================================
$config = @{ $config = @{
# Reports App sources
BackendSource = "../../reports-app/backend" BackendSource = "../../reports-app/backend"
FrontendSource = "../../reports-app/frontend" FrontendSource = "../../reports-app/frontend"
TelegramBotSource = "../../reports-app/telegram-bot" TelegramBotSource = "../../reports-app/telegram-bot"
# Data Entry App sources
DataEntryBackendSource = "../../data-entry-app/backend"
DataEntryFrontendSource = "../../data-entry-app/frontend"
# Shared sources
SharedSource = "../../shared" SharedSource = "../../shared"
ConfigSource = "../config" ConfigSource = "../config"
RequiredNodeVersion = 16 RequiredNodeVersion = 16
@@ -175,17 +180,25 @@ function Show-BuildMenu {
Write-Host " Select components to build:" -ForegroundColor Yellow Write-Host " Select components to build:" -ForegroundColor Yellow
Write-Host "" Write-Host ""
Write-Host " [1] All Components" -ForegroundColor White 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 ""
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 " (Vue.js build + FastAPI backend files)" -ForegroundColor Gray
Write-Host "" 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 " (FastAPI backend files + shared modules)" -ForegroundColor Gray
Write-Host "" Write-Host ""
Write-Host " [4] Telegram Bot Only" -ForegroundColor White Write-Host " [4] Telegram Bot Only" -ForegroundColor White
Write-Host " (Telegram bot standalone package)" -ForegroundColor Gray Write-Host " (Telegram bot standalone package)" -ForegroundColor Gray
Write-Host "" 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 " [C] Clean Build Cache" -ForegroundColor Yellow
Write-Host " (Remove cached node_modules to free disk space)" -ForegroundColor Gray Write-Host " (Remove cached node_modules to free disk space)" -ForegroundColor Gray
Write-Host "" Write-Host ""
@@ -202,6 +215,8 @@ function Show-BuildMenu {
"2" { return "Frontend" } "2" { return "Frontend" }
"3" { return "Backend" } "3" { return "Backend" }
"4" { return "TelegramBot" } "4" { return "TelegramBot" }
"5" { return "DataEntryApp" }
"6" { return "DataEntryBackend" }
"C" { "C" {
Clear-BuildCache Clear-BuildCache
Write-Host "`nPress any key to return to menu..." -ForegroundColor Gray Write-Host "`nPress any key to return to menu..." -ForegroundColor Gray
@@ -213,7 +228,7 @@ function Show-BuildMenu {
exit 0 exit 0
} }
default { 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) } while ($true)
@@ -258,18 +273,28 @@ function Build-Frontend {
throw "Frontend source path not found: $SourcePath" throw "Frontend source path not found: $SourcePath"
} }
# Create temporary build directory # Determine if this is reports-app or data-entry-app based on source path
$tempBuildDir = Join-Path $OutputPath ".temp-frontend-build" $appFolder = Split-Path (Split-Path $SourcePath -Parent) -Leaf # "reports-app" or "data-entry-app"
if (Test-Path $tempBuildDir) {
# 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..." 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 New-Item -ItemType Directory -Path $tempBuildDir -Force | Out-Null
Write-Success "Created temp build directory (isolated from WSL)" Write-Success "Created temp build directory (isolated from WSL)"
# Create cache directory for node_modules (OUTSIDE deploy-package) # Create cache directory for node_modules (OUTSIDE deploy-package)
$scriptDir = Split-Path -Parent $PSScriptRoot $scriptDir = Split-Path -Parent $PSScriptRoot
$cacheDir = Join-Path $scriptDir ".build-cache" $cacheDir = Join-Path $scriptDir ".build-cache-$appFolder"
if (-not (Test-Path $cacheDir)) { if (-not (Test-Path $cacheDir)) {
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
} }
@@ -318,7 +343,30 @@ function Build-Frontend {
} }
Write-Success "Frontend sources copied to temp" 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 Push-Location $tempBuildDir
try { try {
# Check if dependencies need to be reinstalled # Check if dependencies need to be reinstalled
@@ -386,6 +434,24 @@ function Build-Frontend {
throw "Vite not found in node_modules - devDependencies not installed" 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 # Build for production
Write-Step "Building for production..." Write-Step "Building for production..."
$env:NODE_ENV = "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 { function Copy-SharedModules {
param( param(
[string]$SourcePath, [string]$SourcePath,
@@ -726,10 +898,12 @@ function New-DeploymentReadme {
Write-Step "Creating deployment README..." Write-Step "Creating deployment README..."
$componentDesc = switch ($ComponentType) { $componentDesc = switch ($ComponentType) {
"All" { "COMPLETE DEPLOYMENT PACKAGE (Frontend + Backend + Telegram Bot)" } "All" { "COMPLETE DEPLOYMENT PACKAGE (Reports + Telegram + Data Entry)" }
"Frontend" { "FRONTEND + BACKEND DEPLOYMENT PACKAGE" } "Frontend" { "REPORTS FRONTEND + BACKEND DEPLOYMENT PACKAGE" }
"Backend" { "BACKEND DEPLOYMENT PACKAGE" } "Backend" { "REPORTS BACKEND DEPLOYMENT PACKAGE" }
"TelegramBot" { "TELEGRAM BOT DEPLOYMENT PACKAGE" } "TelegramBot" { "TELEGRAM BOT DEPLOYMENT PACKAGE" }
"DataEntryApp" { "DATA ENTRY APP DEPLOYMENT PACKAGE (Frontend + Backend)" }
"DataEntryBackend" { "DATA ENTRY BACKEND DEPLOYMENT PACKAGE" }
} }
$readme = @" $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 += @" $readme += @"
backend/ FastAPI backend application (Python) backend/ Reports App - FastAPI backend (Python, port 8000)
frontend/ Vue.js static files (production build) frontend/ Reports App - Vue.js static files (production build)
shared/ Shared Python modules (auth, database, utils) shared/ Shared Python modules (auth, database, utils)
config/ Configuration templates (.env, web.config) config/ Configuration templates (.env, web.config)
"@ "@
@@ -756,7 +930,20 @@ CONTENTS:
if ($ComponentType -eq "All" -or $ComponentType -eq "TelegramBot") { if ($ComponentType -eq "All" -or $ComponentType -eq "TelegramBot") {
$readme += @" $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 # Build based on component type
switch ($ComponentType) { switch ($ComponentType) {
"All" { "All" {
# Frontend # Reports Frontend
$frontendDistPath = Build-Frontend -SourcePath $Paths.FrontendSource -OutputPath $OutputPath $frontendDistPath = Build-Frontend -SourcePath $Paths.FrontendSource -OutputPath $OutputPath
$frontendDest = Join-Path $OutputPath "frontend" $frontendDest = Join-Path $OutputPath "frontend"
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null 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 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" $backendDest = Join-Path $OutputPath "backend"
Copy-BackendFiles -SourcePath $Paths.BackendSource -DestPath $backendDest Copy-BackendFiles -SourcePath $Paths.BackendSource -DestPath $backendDest
@@ -945,6 +1132,18 @@ function New-DeploymentPackage {
# Telegram Bot # Telegram Bot
$telegramDest = Join-Path $OutputPath "telegram-bot" $telegramDest = Join-Path $OutputPath "telegram-bot"
Copy-TelegramBotFiles -SourcePath $Paths.TelegramBotSource -DestPath $telegramDest 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" { "Frontend" {
@@ -988,10 +1187,46 @@ function New-DeploymentPackage {
$telegramDest = Join-Path $OutputPath "telegram-bot" $telegramDest = Join-Path $OutputPath "telegram-bot"
Copy-TelegramBotFiles -SourcePath $Paths.TelegramBotSource -DestPath $telegramDest 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 # 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 Remove-TempDirectories -OutputPath $OutputPath
} }
@@ -1041,9 +1276,14 @@ function Main {
try { try {
# Resolve paths # Resolve paths
$paths = @{ $paths = @{
# Reports App sources
BackendSource = Resolve-FullPath -Path $config.BackendSource BackendSource = Resolve-FullPath -Path $config.BackendSource
FrontendSource = Resolve-FullPath -Path $config.FrontendSource FrontendSource = Resolve-FullPath -Path $config.FrontendSource
TelegramBotSource = Resolve-FullPath -Path $config.TelegramBotSource 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 SharedSource = Resolve-FullPath -Path $config.SharedSource
ConfigSource = Resolve-FullPath -Path $config.ConfigSource ConfigSource = Resolve-FullPath -Path $config.ConfigSource
} }
@@ -1055,7 +1295,7 @@ function Main {
Write-Host " Output Path: $outputFullPath" Write-Host " Output Path: $outputFullPath"
# Validate Node.js for frontend builds # 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 Test-NodeJS
} }

View File

@@ -315,6 +315,7 @@ function Invoke-Deployment {
Services = @{ Services = @{
backend = "Running" backend = "Running"
telegramBot = "Running" telegramBot = "Running"
dataEntry = "Running"
} }
} }

View File

@@ -21,7 +21,7 @@
- ViewConfig: Display current configuration - ViewConfig: Display current configuration
.PARAMETER Component .PARAMETER Component
Component to build (All, Frontend, Backend, TelegramBot) Component to build (All, Frontend, Backend, TelegramBot, DataEntryApp, DataEntryBackend)
.PARAMETER TransferMethod .PARAMETER TransferMethod
Transfer method (Auto, WindowsShare, SSH) Transfer method (Auto, WindowsShare, SSH)
@@ -41,9 +41,13 @@
.\Publish-And-Deploy.ps1 -NonInteractive -Action Build -Component Frontend -TransferMethod SSH .\Publish-And-Deploy.ps1 -NonInteractive -Action Build -Component Frontend -TransferMethod SSH
Build frontend and publish via 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 .NOTES
Author: ROA2WEB Team 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 Requires: PowerShell 5.1+, SSH key configured for remote access
#> #>
@@ -54,7 +58,7 @@ param(
[ValidateSet("Build", "TestConnections", "ViewConfig")] [ValidateSet("Build", "TestConnections", "ViewConfig")]
[string]$Action = "", [string]$Action = "",
[ValidateSet("All", "Frontend", "Backend", "TelegramBot")] [ValidateSet("All", "Frontend", "Backend", "TelegramBot", "DataEntryApp", "DataEntryBackend")]
[string]$Component = "", [string]$Component = "",
[ValidateSet("Auto", "WindowsShare", "SSH")] [ValidateSet("Auto", "WindowsShare", "SSH")]
@@ -295,7 +299,7 @@ function Test-AllConnections {
function Invoke-Build { function Invoke-Build {
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[ValidateSet("All", "Frontend", "Backend", "TelegramBot")] [ValidateSet("All", "Frontend", "Backend", "TelegramBot", "DataEntryApp", "DataEntryBackend")]
[string]$Component [string]$Component
) )
@@ -554,7 +558,8 @@ function Edit-Configuration {
} }
"4" { "4" {
Write-Host "`nEdit Build Settings:" -ForegroundColor Yellow 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 $newComp = Read-Host
if ($newComp) { $script:Config.build.defaultComponent = $newComp } if ($newComp) { $script:Config.build.defaultComponent = $newComp }
@@ -658,17 +663,25 @@ function Show-ComponentMenu {
Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host "" Write-Host ""
Write-Host " [1] All Components" -ForegroundColor White 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 ""
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 " (Vue.js build + FastAPI backend)" -ForegroundColor Gray
Write-Host "" 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 " (FastAPI backend + shared modules)" -ForegroundColor Gray
Write-Host "" Write-Host ""
Write-Host " [4] Telegram Bot Only" -ForegroundColor White Write-Host " [4] Telegram Bot Only" -ForegroundColor White
Write-Host " (Telegram bot standalone package)" -ForegroundColor Gray Write-Host " (Telegram bot standalone package)" -ForegroundColor Gray
Write-Host "" 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 " [B] Back to Main Menu" -ForegroundColor Yellow
Write-Host "" Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan
@@ -682,9 +695,11 @@ function Show-ComponentMenu {
"2" { return "Frontend" } "2" { return "Frontend" }
"3" { return "Backend" } "3" { return "Backend" }
"4" { return "TelegramBot" } "4" { return "TelegramBot" }
"5" { return "DataEntryApp" }
"6" { return "DataEntryBackend" }
"B" { return "Back" } "B" { return "Back" }
default { 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) } while ($true)

View File

@@ -42,8 +42,9 @@
param( param(
[switch]$NonInteractive, [switch]$NonInteractive,
[ValidateSet("DeployBackend", "DeployTelegramBot", "DeployAll", [ValidateSet("DeployBackend", "DeployTelegramBot", "DeployDataEntry", "DeployAll",
"StartAll", "StopAll", "RestartAll", "Status")] "StartAll", "StopAll", "RestartAll",
"StartDataEntry", "StopDataEntry", "RestartDataEntry", "Status")]
[string]$Action = "" [string]$Action = ""
) )
@@ -72,16 +73,16 @@ $script:Config = @{
SourcePath = $detectedSourcePath SourcePath = $detectedSourcePath
BackupPath = "C:\inetpub\wwwroot\roa2web\backups" BackupPath = "C:\inetpub\wwwroot\roa2web\backups"
# Backend # Reports Backend
BackendPath = "C:\inetpub\wwwroot\roa2web\backend" BackendPath = "C:\inetpub\wwwroot\roa2web\backend"
BackendServiceName = "ROA2WEB-Backend" BackendServiceName = "ROA2WEB-Backend"
BackendServiceDisplayName = "ROA2WEB Backend API" BackendServiceDisplayName = "ROA2WEB Backend API"
BackendServiceDescription = "FastAPI backend service for ROA2WEB application" BackendServiceDescription = "FastAPI backend service for ROA2WEB Reports application"
BackendPort = 8000 BackendPort = 8000
BackendHealthUrl = "http://localhost:8000/health" BackendHealthUrl = "http://localhost:8000/health"
BackendHealthTimeout = 5 BackendHealthTimeout = 5
# Frontend # Reports Frontend
FrontendPath = "C:\inetpub\wwwroot\roa2web\frontend" FrontendPath = "C:\inetpub\wwwroot\roa2web\frontend"
# IIS Configuration # IIS Configuration
@@ -98,9 +99,24 @@ $script:Config = @{
TelegramBotHealthUrl = "http://localhost:8002/internal/health" TelegramBotHealthUrl = "http://localhost:8002/internal/health"
TelegramBotHealthTimeout = 10 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 # Logs
LogsPath = "C:\inetpub\wwwroot\roa2web\logs" LogsPath = "C:\inetpub\wwwroot\roa2web\logs"
TelegramBotLogsPath = "C:\inetpub\wwwroot\roa2web\telegram-bot\logs" TelegramBotLogsPath = "C:\inetpub\wwwroot\roa2web\telegram-bot\logs"
DataEntryLogsPath = "C:\inetpub\wwwroot\roa2web\data-entry-backend\logs"
# Timeouts # Timeouts
ServiceTimeout = 30 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 { function Get-ServiceSafe {
param([string]$ServiceName) param([string]$ServiceName)
try { 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 # MENU FUNCTIONS
# ============================================================================= # =============================================================================
@@ -872,10 +1168,10 @@ function Show-MainMenu {
Write-Host " Main Menu:" -ForegroundColor Yellow Write-Host " Main Menu:" -ForegroundColor Yellow
Write-Host "" Write-Host ""
Write-Host " [1] Deploy Components" -ForegroundColor White 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 ""
Write-Host " [2] Manage Services" -ForegroundColor White 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 ""
Write-Host " [3] Check Status" -ForegroundColor White Write-Host " [3] Check Status" -ForegroundColor White
Write-Host " (View service status and health checks)" -ForegroundColor Gray Write-Host " (View service status and health checks)" -ForegroundColor Gray
@@ -908,14 +1204,20 @@ function Show-DeployMenu {
Write-Host "" Write-Host ""
Write-Host " Select what to deploy:" -ForegroundColor Yellow Write-Host " Select what to deploy:" -ForegroundColor Yellow
Write-Host "" 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 " (FastAPI backend + Vue.js frontend files)" -ForegroundColor Gray
Write-Host "" Write-Host ""
Write-Host " [2] Telegram Bot" -ForegroundColor White Write-Host " [2] Telegram Bot" -ForegroundColor White
Write-Host " (Telegram bot application only)" -ForegroundColor Gray Write-Host " (Telegram bot application only)" -ForegroundColor Gray
Write-Host "" Write-Host ""
Write-Host " [3] All Components" -ForegroundColor White Write-Host " --- Data Entry App ---" -ForegroundColor Cyan
Write-Host " (Backend + Frontend + Telegram Bot)" -ForegroundColor Gray 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 ""
Write-Host " [B] Back to Main Menu" -ForegroundColor Yellow Write-Host " [B] Back to Main Menu" -ForegroundColor Yellow
Write-Host "" Write-Host ""
@@ -928,10 +1230,11 @@ function Show-DeployMenu {
switch ($choice.ToUpper()) { switch ($choice.ToUpper()) {
"1" { return "Backend" } "1" { return "Backend" }
"2" { return "TelegramBot" } "2" { return "TelegramBot" }
"3" { return "All" } "3" { return "DataEntry" }
"4" { return "All" }
"B" { return "Back" } "B" { return "Back" }
default { 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) } while ($true)
@@ -948,16 +1251,21 @@ function Show-ManageMenu {
Write-Host " [2] Stop All Services" -ForegroundColor Red Write-Host " [2] Stop All Services" -ForegroundColor Red
Write-Host " [3] Restart All Services" -ForegroundColor Yellow Write-Host " [3] Restart All Services" -ForegroundColor Yellow
Write-Host "" Write-Host ""
Write-Host " Backend Service:" -ForegroundColor Yellow Write-Host " Reports Backend Service:" -ForegroundColor Yellow
Write-Host " [4] Start Backend" -ForegroundColor Green Write-Host " [4] Start Reports Backend" -ForegroundColor Green
Write-Host " [5] Stop Backend" -ForegroundColor Red Write-Host " [5] Stop Reports Backend" -ForegroundColor Red
Write-Host " [6] Restart Backend" -ForegroundColor Yellow Write-Host " [6] Restart Reports Backend" -ForegroundColor Yellow
Write-Host "" Write-Host ""
Write-Host " Telegram Bot Service:" -ForegroundColor Yellow Write-Host " Telegram Bot Service:" -ForegroundColor Yellow
Write-Host " [7] Start Telegram Bot" -ForegroundColor Green Write-Host " [7] Start Telegram Bot" -ForegroundColor Green
Write-Host " [8] Stop Telegram Bot" -ForegroundColor Red Write-Host " [8] Stop Telegram Bot" -ForegroundColor Red
Write-Host " [9] Restart Telegram Bot" -ForegroundColor Yellow Write-Host " [9] Restart Telegram Bot" -ForegroundColor Yellow
Write-Host "" 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 " [B] Back to Main Menu" -ForegroundColor Gray
Write-Host "" Write-Host ""
Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan
@@ -976,9 +1284,12 @@ function Show-ManageMenu {
"7" { return @{ Action = "Start"; Component = "TelegramBot" } } "7" { return @{ Action = "Start"; Component = "TelegramBot" } }
"8" { return @{ Action = "Stop"; Component = "TelegramBot" } } "8" { return @{ Action = "Stop"; Component = "TelegramBot" } }
"9" { return @{ Action = "Restart"; 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 = "" } } "B" { return @{ Action = "Back"; Component = "" } }
default { 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) } while ($true)
@@ -1496,6 +1807,18 @@ function Deploy-Backend {
# Update files # Update files
Update-BackendFiles 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 Update-FrontendFiles
# Start backend service # Start backend service
@@ -1558,10 +1881,12 @@ function Deploy-TelegramBot {
$originalSourcePath = $Config.SourcePath $originalSourcePath = $Config.SourcePath
$Config.SourcePath = Join-Path $originalSourcePath "telegram-bot" $Config.SourcePath = Join-Path $originalSourcePath "telegram-bot"
Update-TelegramBotFiles try {
Update-TelegramBotFiles
# Restore original source path } finally {
$Config.SourcePath = $originalSourcePath # ALWAYS restore original source path (even on error)
$Config.SourcePath = $originalSourcePath
}
# Start telegram bot service # Start telegram bot service
$started = Start-ServiceComponent -ComponentName "Telegram Bot" ` $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 # MAIN EXECUTION FLOW
# ============================================================================= # =============================================================================
@@ -1601,15 +2217,17 @@ function Execute-ManageAction {
switch ($Component) { switch ($Component) {
"All" { "All" {
$components = @( $components = @(
@{ Name = "Backend"; ServiceName = $Config.BackendServiceName; @{ Name = "Reports Backend"; ServiceName = $Config.BackendServiceName;
HealthUrl = $Config.BackendHealthUrl; HealthTimeout = $Config.BackendHealthTimeout }, HealthUrl = $Config.BackendHealthUrl; HealthTimeout = $Config.BackendHealthTimeout },
@{ Name = "Telegram Bot"; ServiceName = $Config.TelegramBotServiceName; @{ 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" { "Backend" {
$components = @( $components = @(
@{ Name = "Backend"; ServiceName = $Config.BackendServiceName; @{ Name = "Reports Backend"; ServiceName = $Config.BackendServiceName;
HealthUrl = $Config.BackendHealthUrl; HealthTimeout = $Config.BackendHealthTimeout } HealthUrl = $Config.BackendHealthUrl; HealthTimeout = $Config.BackendHealthTimeout }
) )
} }
@@ -1619,6 +2237,12 @@ function Execute-ManageAction {
HealthUrl = $Config.TelegramBotHealthUrl; HealthTimeout = $Config.TelegramBotHealthTimeout } HealthUrl = $Config.TelegramBotHealthUrl; HealthTimeout = $Config.TelegramBotHealthTimeout }
) )
} }
"DataEntry" {
$components = @(
@{ Name = "Data Entry"; ServiceName = $Config.DataEntryServiceName;
HealthUrl = $Config.DataEntryHealthUrl; HealthTimeout = $Config.DataEntryHealthTimeout }
)
}
} }
foreach ($comp in $components) { foreach ($comp in $components) {
@@ -1665,11 +2289,16 @@ function Execute-DeployAction {
$result = Deploy-TelegramBot $result = Deploy-TelegramBot
$result # Explicitly output the boolean $result # Explicitly output the boolean
} }
"DataEntry" {
$result = Deploy-DataEntry
$result # Explicitly output the boolean
}
"All" { "All" {
$backendOk = Deploy-Backend $backendOk = Deploy-Backend
$telegramOk = Deploy-TelegramBot $telegramOk = Deploy-TelegramBot
$dataEntryOk = Deploy-DataEntry
# Return combined result # 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 " ROA2WEB - System Status" -ForegroundColor Cyan
Write-Host ("=" * 70) -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor Cyan
Show-ServiceStatus -ComponentName "Backend" ` Show-ServiceStatus -ComponentName "Reports Backend" `
-ServiceName $Config.BackendServiceName ` -ServiceName $Config.BackendServiceName `
-HealthUrl $Config.BackendHealthUrl ` -HealthUrl $Config.BackendHealthUrl `
-HealthTimeout $Config.BackendHealthTimeout -HealthTimeout $Config.BackendHealthTimeout
@@ -1705,6 +2334,11 @@ function Show-AllStatus {
-HealthUrl $Config.TelegramBotHealthUrl ` -HealthUrl $Config.TelegramBotHealthUrl `
-HealthTimeout $Config.TelegramBotHealthTimeout -HealthTimeout $Config.TelegramBotHealthTimeout
Show-ServiceStatus -ComponentName "Data Entry" `
-ServiceName $Config.DataEntryServiceName `
-HealthUrl $Config.DataEntryHealthUrl `
-HealthTimeout $Config.DataEntryHealthTimeout
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
} }
@@ -1722,10 +2356,14 @@ function Main {
$success = switch ($Action) { $success = switch ($Action) {
"DeployBackend" { Execute-DeployAction -Component "Backend" } "DeployBackend" { Execute-DeployAction -Component "Backend" }
"DeployTelegramBot" { Execute-DeployAction -Component "TelegramBot" } "DeployTelegramBot" { Execute-DeployAction -Component "TelegramBot" }
"DeployDataEntry" { Execute-DeployAction -Component "DataEntry" }
"DeployAll" { Execute-DeployAction -Component "All" } "DeployAll" { Execute-DeployAction -Component "All" }
"StartAll" { Execute-ManageAction -Action "Start" -Component "All"; $true } "StartAll" { Execute-ManageAction -Action "Start" -Component "All"; $true }
"StopAll" { Execute-ManageAction -Action "Stop" -Component "All"; $true } "StopAll" { Execute-ManageAction -Action "Stop" -Component "All"; $true }
"RestartAll" { Execute-ManageAction -Action "Restart" -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 } "Status" { Show-AllStatus; $true }
default { $false } default { $false }
} }

View File

@@ -20,5 +20,33 @@
"deployment": { "deployment": {
"autoDeployEnabled": true, "autoDeployEnabled": true,
"checkIntervalMinutes": 5 "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
}
} }
} }