fix: IIS sub-application deployment for production

Fixes 3 critical issues preventing production deployment on Windows IIS:

1. **IIS Sub-Application Path Stripping**
   - Changed URL patterns from ^roa2web/api/(.*) to ^api/(.*)
   - IIS sub-app at /roa2web automatically strips prefix
   - Requests arrive as /api/* not /roa2web/api/*

2. **SPA Fallback Absolute Path**
   - Changed from url="/index.html" to url="index.html"
   - Absolute paths (/) refer to site root, not sub-app
   - Relative path correctly serves from sub-app

3. **MIME Type Duplicates (500 Error)**
   - Added <remove> before <mimeMap> for .js, .json, .webmanifest
   - Prevents "duplicate collection entry" errors
   - Allows override of server-level MIME types

Build Script Improvements:
- Build-ROA2WEB.ps1: Copy public/ folder to temp build dir
- Build-ROA2WEB.ps1: Added verification logging for web.config
- ROA2WEB-Console.ps1: Fixed web.config verification location

Cleanup:
- Removed outdated web.config.10.0.20.36-INTERNAL
- Removed temporary test files and docs

Tested: https://roa2web.romfast.ro/roa2web/ - login page loads successfully

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 16:38:47 +02:00
parent 9008876b16
commit ce85e0643b
6 changed files with 95 additions and 830 deletions

View File

@@ -1,371 +0,0 @@
# Deployment Fixes - 2025-12-29
## Summary
Fixed critical Telegram bot deployment issue and updated all deployment scripts and documentation to use `--workers 1` configuration.
---
## Problems Fixed
### 1. ❌ Telegram Bot Conflict (CRITICAL)
**Error:**
```
telegram.error.Conflict: Conflict: terminated by other getUpdates request;
make sure that only one bot instance is running
```
**Root Cause:**
- NSSM service was configured with `--workers 4`
- Each uvicorn worker spawned its own Telegram bot instance
- Telegram API allows only ONE bot instance to poll for updates
- Multiple instances conflicted → continuous errors
**Solution:**
- Changed NSSM service configuration to `--workers 1`
- Single worker = Single bot instance = No conflicts
---
### 2. ❌ Cache Stats Endpoint (502 Bad Gateway)
**Error:**
- `/api/reports/cache/stats` returned 502 Bad Gateway
**Root Cause:**
- Multiple workers accessing same SQLite cache database
- SQLite locking conflicts caused worker crashes
**Solution:**
- **Automatically fixed** by using `--workers 1`
- Single worker = No SQLite locking conflicts
---
### 3. ⚠️ Pydantic v2 Warnings
**Warning:**
```
UserWarning: Valid config keys have changed in V2:
* 'schema_extra' has been renamed to 'json_schema_extra'
```
**Solution:**
- Updated `backend/modules/reports/models/trial_balance.py:72`
- Changed `schema_extra``json_schema_extra`
---
## Files Modified
### 1. Deployment Scripts
#### `Install-ROA2WEB.ps1` (Line 370-371)
**Changed:**
```powershell
# Before
& nssm install ... "--workers" "4"
# After
# NOTE: Using --workers 1 because Telegram bot requires single instance (polling conflict)
& nssm install ... "--workers" "1"
```
#### `Fix-TelegramWorkers.ps1` (NEW)
**Created:** Automatic fix script for existing installations
- Stops ROA2WEB-Backend service
- Updates NSSM parameters to `--workers 1`
- Restarts service
- Verifies configuration and health
- Checks logs for Telegram errors
**Usage:**
```powershell
cd C:\TEMP\ROA2WEB-Scripts
.\Fix-TelegramWorkers.ps1
```
---
### 2. Documentation
#### `TELEGRAM-BOT-DEPLOYMENT.md` (NEW)
**Created:** Complete deployment guide for Telegram bot
- Explains the worker conflict issue
- Installation and upgrade procedures
- Verification steps
- Troubleshooting guide
- Architecture notes and performance analysis
**Key Points:**
- ✅ Always use `--workers 1`
- ✅ Performance is excellent (async I/O bound, not CPU bound)
- ✅ Single worker handles 100+ concurrent users
- ✅ Lower memory usage (~400 MB vs ~1.6 GB)
#### `WINDOWS_DEPLOYMENT.md`
**Updated sections:**
1. **High CPU/Memory Usage** (Line 716-743)
- Removed outdated `WORKERS=2` in .env suggestion
- Clarified workers are configured in NSSM, not .env
- Added warning about not changing `--workers 1`
2. **Backend Configuration (.env)** (Line 353-360)
- Removed `WORKERS=4` from example .env
- Added note that WORKERS is configured in NSSM
- Added reference to TELEGRAM-BOT-DEPLOYMENT.md
---
### 3. Backend Code
#### `backend/modules/reports/models/trial_balance.py` (Line 72)
**Changed:**
```python
# Before
class Config:
schema_extra = { ... }
# After
class Config:
json_schema_extra = { ... }
```
**Impact:** Eliminates Pydantic v2 warnings in logs
---
## Deployment Instructions
### For New Installations
Use updated `Install-ROA2WEB.ps1`:
```powershell
.\Install-ROA2WEB.ps1
```
The script now automatically uses `--workers 1` - no manual configuration needed.
---
### For Existing Installations
**Option 1: Automatic Fix (Recommended)**
```powershell
cd C:\TEMP\ROA2WEB-Scripts
.\Fix-TelegramWorkers.ps1
```
**Option 2: Manual Fix**
```powershell
# Stop service
Stop-Service ROA2WEB-Backend
# Update NSSM
nssm set ROA2WEB-Backend AppParameters "-m uvicorn main:app --host 127.0.0.1 --port 8000 --workers 1"
# Verify
nssm get ROA2WEB-Backend AppParameters
# Start service
Start-Service ROA2WEB-Backend
# Monitor logs
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 50 -Wait
```
---
## Verification
### 1. Check NSSM Configuration
```powershell
nssm get ROA2WEB-Backend AppParameters
```
**Expected:**
```
-m uvicorn main:app --host 127.0.0.1 --port 8000 --workers 1
```
### 2. Check Process Count
```powershell
Get-Process -Name python
```
**Expected:**
- 1-2 Python processes (1 parent + 1 worker, or just 1 combined)
- **NOT** 5 processes (1 parent + 4 workers)
### 3. Check Telegram Bot in Logs
```powershell
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 100 | Select-String "Bot running"
```
**Expected:**
- Message appears **EXACTLY ONCE**
- No "Conflict: terminated by other getUpdates" errors
### 4. Test Cache Stats Endpoint
```
https://roa2web.romfast.ro/roa2web/reports/cache-stats
```
**Expected:**
- Page loads successfully (200 OK)
- No 502 Bad Gateway error
### 5. Test Telegram Bot
Send a message to @ROA2WEBBot in Telegram
**Expected:**
- Bot responds without errors
- No conflict errors in backend logs
---
## Performance Impact
### Before Fix (--workers 4)
- ❌ Telegram bot conflicts (unusable)
- ❌ Cache stats endpoint crashes (502)
- 📊 5 Python processes
- 📊 ~1.6 GB memory usage
- ✅ Same performance (async I/O)
### After Fix (--workers 1)
- ✅ Telegram bot works perfectly
- ✅ Cache stats endpoint works
- ✅ No SQLite locking conflicts
- 📊 2 Python processes
- 📊 ~400 MB memory usage
- ✅ Same performance (async I/O)
**Conclusion:** `--workers 1` is **SUPERIOR** for this application in every way.
---
## Architecture Notes
### Why Single Worker is Better
1. **Telegram Bot Requirement**
- Telegram API allows only ONE bot instance per token
- Multiple workers = Multiple bot instances = Conflicts
2. **SQLite Cache Database**
- Shared SQLite database for cache
- Multiple workers = Locking conflicts = Crashes
3. **Async I/O Performance**
- Application is I/O bound (Oracle database queries)
- NOT CPU bound
- Single worker with async/await handles 100+ concurrent requests
- Multiple workers provide NO performance benefit
4. **Lower Resource Usage**
- Less memory (~400 MB vs ~1.6 GB)
- Fewer processes to manage
- Simpler debugging
### Performance Characteristics
**With `--workers 1`:**
- Concurrent requests: 100+ (async/await)
- Database pooling: 5-10 Oracle connections (shared)
- Memory usage: ~300-500 MB per worker
- CPU usage: Low (I/O bound)
- Response times: 50-200ms (mostly DB query time)
**Adequate for:**
- 50-100 concurrent users
- 1000+ requests per minute
- Multiple modules (Reports, Data Entry, Telegram)
---
## Future Considerations
### Alternative: Separate Bot Service
If scalability becomes an issue, consider:
**Option A (Current):** Integrated bot, single worker
- ✅ Simple deployment
- ✅ Single service to manage
- ⚠️ Must use `--workers 1`
**Option B (Alternative):** Separate bot service
- ✅ Could use `--workers 4` for web backend
- ❌ Two Windows services to manage
- ❌ More complex deployment
- ❌ Two processes to monitor
**Decision:** Keep current architecture. Performance is excellent and deployment is simpler.
---
## Checklist for Deployment
### Pre-Deployment
- [ ] Read this document
- [ ] Read `TELEGRAM-BOT-DEPLOYMENT.md`
- [ ] Backup current installation
- [ ] Note current NSSM parameters
### Deployment
- [ ] Copy updated scripts to server
- [ ] Run `Fix-TelegramWorkers.ps1` (existing) OR `Install-ROA2WEB.ps1` (new)
- [ ] Wait 30 seconds for service startup
- [ ] Verify NSSM parameters show `--workers 1`
### Post-Deployment Verification
- [ ] Check process count (should be 1-2, not 5)
- [ ] Check logs for single bot startup message
- [ ] Verify NO "Conflict" errors in logs
- [ ] Test health endpoint: http://localhost:8000/health
- [ ] Test cache stats: https://roa2web.romfast.ro/roa2web/reports/cache-stats
- [ ] Test Telegram bot functionality
- [ ] Monitor logs for 5 minutes
### If Issues Occur
- [ ] Check `TELEGRAM-BOT-DEPLOYMENT.md` troubleshooting section
- [ ] Review backend logs for errors
- [ ] Verify Oracle connection
- [ ] Check PYTHONPATH in NSSM config
- [ ] Contact development team with logs
---
## Rollback Procedure
If you need to rollback (not recommended):
```powershell
# Stop service
Stop-Service ROA2WEB-Backend
# Restore old parameters (WILL BREAK TELEGRAM BOT)
nssm set ROA2WEB-Backend AppParameters "-m uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4"
# Start service
Start-Service ROA2WEB-Backend
```
**Warning:** Rollback will restore the Telegram bot conflict issue.
---
## Support
For questions or issues:
1. Check `TELEGRAM-BOT-DEPLOYMENT.md` troubleshooting
2. Review `WINDOWS_DEPLOYMENT.md` for general deployment issues
3. Check backend logs: `C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log`
4. Contact development team with:
- Current NSSM parameters
- Recent log excerpts (last 100 lines)
- Process count and memory usage
- Specific error messages

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
====================================================================
ROA2WEB - Internal Application Server Configuration
====================================================================
Server: 10.0.20.36 (internal application server)
Role: Application host + API proxy to localhost backend
This web.config ONLY goes on the INTERNAL server (10.0.20.36).
It serves the frontend and proxies API calls to localhost:8000.
Location: C:\inetpub\wwwroot\roa2web\web.config (on 10.0.20.36)
⚠️ DO NOT use this config on 10.0.20.122 (public server)!
====================================================================
-->
<configuration>
<system.webServer>
<rewrite>
<rules>
<!-- Proxy API requests to unified backend on localhost -->
<rule name="Proxy Unified API" stopProcessing="true">
<match url="^roa2web/api/(.*)" />
<action type="Rewrite" url="http://localhost:8000/api/{R:1}" />
</rule>
<!-- Proxy uploads to unified backend on localhost -->
<rule name="Proxy Uploads" stopProcessing="true">
<match url="^roa2web/uploads/(.*)" />
<action type="Rewrite" url="http://localhost:8000/uploads/{R:1}" />
</rule>
<!-- SPA fallback - all other routes serve index.html -->
<rule name="SPA Fallback" stopProcessing="true">
<match url="^roa2web/.*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/roa2web/index.html" />
</rule>
</rules>
</rewrite>
<!-- Static content configuration -->
<staticContent>
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
<mimeMap fileExtension=".js" mimeType="application/javascript" />
<mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
<!-- Client cache for static assets (1 year) -->
<httpProtocol>
<customHeaders>
<add name="Cache-Control" value="public, max-age=31536000" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>

View File

@@ -117,6 +117,11 @@ function Write-Warning {
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Write-Info {
param([string]$Message)
Write-Host " [INFO] $Message" -ForegroundColor Gray
}
function Resolve-FullPath {
param([string]$Path)
@@ -352,6 +357,26 @@ function Build-Frontend {
}
}
# Copy public/ folder (contains web.config and other static assets)
$publicSourcePath = Join-Path $projectRoot "public"
if (Test-Path $publicSourcePath) {
$publicDestPath = Join-Path $tempBuildDir "public"
Write-Step "Copying public/ folder..."
Write-Info "Source: $publicSourcePath"
Write-Info "Dest: $publicDestPath"
Copy-Item -Path $publicSourcePath -Destination $publicDestPath -Recurse -Force
Write-Success "public/ folder copied (includes web.config)"
# Verify web.config was copied
$copiedWebConfig = Join-Path $publicDestPath "web.config"
if (Test-Path $copiedWebConfig) {
Write-Success "web.config found in public/"
} else {
Write-Warning "web.config NOT found in public/ - check source"
}
} else {
Write-Warning "public/ folder not found at: $publicSourcePath"
}
# Copy shared folder to maintain relative imports (../shared/)
# For ultrathin monolith: src/ and shared/ are siblings at project root
@@ -463,6 +488,15 @@ function Build-Frontend {
$totalSize = ($distFiles | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Success "Generated $(($distFiles).Count) files ($([math]::Round($totalSize, 2)) MB)"
# Verify web.config was built
$webConfigPath = Join-Path $distPath "web.config"
if (Test-Path $webConfigPath) {
Write-Success "web.config found in build output"
} else {
Write-Warning "web.config NOT found in build output: $webConfigPath"
Write-Warning "Check if public/web.config exists in source"
}
return $distPath
} finally {
Pop-Location
@@ -879,7 +913,18 @@ function New-DeploymentPackage {
$frontendDest = Join-Path $OutputPath "frontend"
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
Write-Step "Copying Unified Frontend files (SPA)..."
Write-Info "Source: $frontendDistPath"
Write-Info "Destination: $frontendDest"
Copy-Item -Path "$frontendDistPath\*" -Destination $frontendDest -Recurse -Force
# Verify web.config was copied
$copiedWebConfig = Join-Path $frontendDest "web.config"
if (Test-Path $copiedWebConfig) {
Write-Success "web.config copied to package"
} else {
Write-Warning "web.config NOT copied to package"
}
Write-Success "Unified Frontend files copied"
# Unified Backend (includes Reports, Data Entry, Telegram modules)
@@ -901,7 +946,18 @@ function New-DeploymentPackage {
$frontendDest = Join-Path $OutputPath "frontend"
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
Write-Step "Copying Unified Frontend files (SPA)..."
Write-Info "Source: $frontendDistPath"
Write-Info "Destination: $frontendDest"
Copy-Item -Path "$frontendDistPath\*" -Destination $frontendDest -Recurse -Force
# Verify web.config was copied
$copiedWebConfig = Join-Path $frontendDest "web.config"
if (Test-Path $copiedWebConfig) {
Write-Success "web.config copied to package"
} else {
Write-Warning "web.config NOT copied to package"
}
Write-Success "Unified Frontend files copied"
# Unified Backend (includes all modules)

View File

@@ -465,15 +465,21 @@ function Deploy-Frontend {
Copy-Item -Path $sourceFe -Destination $Config.FrontendPath -Recurse -Force
Write-Success "Frontend files deployed"
# Copy web.config if present in package
$webConfig = Join-Path $SourcePath "config\web.config"
if (Test-Path $webConfig) {
$destConfig = Join-Path $Config.FrontendPath "web.config"
Copy-Item -Path $webConfig -Destination $destConfig -Force
Write-Success "web.config deployed"
# Verify web.config was deployed (should be in frontend/ from dist/)
$deployedWebConfig = Join-Path $Config.FrontendPath "web.config"
if (Test-Path $deployedWebConfig) {
Write-Success "web.config deployed successfully"
# Verify it has correct configuration for /roa2web/ path
$configContent = Get-Content $deployedWebConfig -Raw
if ($configContent -match 'url="[^"]*roa2web/api') {
Write-Success "web.config contains correct /roa2web/api proxy rules"
} else {
Write-Warning "web.config may not have correct /roa2web/api proxy configuration"
}
} else {
Write-Warning "web.config not found in package: $webConfig"
Write-Warning "web.config not found in deployed frontend: $deployedWebConfig"
Write-Warning "IIS reverse proxy will not work without web.config"
Write-Warning "Ensure 'public/web.config' exists in source and rebuild frontend"
}
Write-Success "Frontend deployment completed successfully"