Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot

Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
This commit is contained in:
2025-10-25 14:55:08 +03:00
commit 6b13ffa183
237 changed files with 70035 additions and 0 deletions

View File

@@ -0,0 +1,361 @@
<#
.SYNOPSIS
Backup ROA2WEB Telegram Bot SQLite Database
.DESCRIPTION
This script creates a backup of the Telegram bot's SQLite database:
- Copies database file with timestamp
- Compresses backup (optional)
- Cleans up old backups (keeps last 30 days)
- Logs backup operations
- Can run manually or via scheduled task
.PARAMETER InstallPath
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
.PARAMETER RetentionDays
Number of days to keep backups (default: 30)
.PARAMETER Compress
Compress backup file (default: true)
.PARAMETER LogToFile
Write backup log to file (default: true)
.EXAMPLE
.\Backup-TelegramDB.ps1
Standard backup with defaults
.EXAMPLE
.\Backup-TelegramDB.ps1 -RetentionDays 60 -Compress $true
Backup with 60-day retention and compression
.EXAMPLE
.\Backup-TelegramDB.ps1 -LogToFile $false
Backup without logging to file (console only)
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+
Can be run manually or via Task Scheduler
#>
[CmdletBinding()]
param(
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
[int]$RetentionDays = 30,
[bool]$Compress = $true,
[bool]$LogToFile = $true
)
$ErrorActionPreference = "Stop"
# =============================================================================
# CONFIGURATION
# =============================================================================
$script:Config = @{
InstallPath = $InstallPath
DataPath = Join-Path $InstallPath "data"
BackupPath = Join-Path $InstallPath "backups"
LogsPath = Join-Path $InstallPath "logs"
DatabaseFile = "telegram_bot.db"
RetentionDays = $RetentionDays
Compress = $Compress
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Log {
param(
[string]$Message,
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] [$Level] $Message"
# Console output
switch ($Level) {
"ERROR" { Write-Host $logMessage -ForegroundColor Red }
"WARN" { Write-Host $logMessage -ForegroundColor Yellow }
"SUCCESS" { Write-Host $logMessage -ForegroundColor Green }
default { Write-Host $logMessage -ForegroundColor Cyan }
}
# File output
if ($LogToFile) {
$logFile = Join-Path $Config.LogsPath "backup.log"
Add-Content -Path $logFile -Value $logMessage -Encoding UTF8
}
}
function Test-DatabaseFile {
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
if (-not (Test-Path $dbPath)) {
Write-Log "Database file not found: $dbPath" -Level "ERROR"
return $false
}
# Check if file is accessible (not locked)
try {
$stream = [System.IO.File]::Open($dbPath, 'Open', 'Read', 'Read')
$stream.Close()
Write-Log "Database file is accessible: $dbPath" -Level "INFO"
return $true
} catch {
Write-Log "Database file is locked or inaccessible: $_" -Level "ERROR"
return $false
}
}
function Get-DatabaseSize {
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
$size = (Get-Item $dbPath).Length / 1KB
return [math]::Round($size, 2)
}
function New-BackupDirectory {
if (-not (Test-Path $Config.BackupPath)) {
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
Write-Log "Created backup directory: $($Config.BackupPath)" -Level "INFO"
}
}
function Backup-Database {
Write-Log "Starting database backup..." -Level "INFO"
# Ensure backup directory exists
New-BackupDirectory
# Generate backup filename with timestamp
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupFileName = "telegram_bot_backup_$timestamp.db"
$backupFilePath = Join-Path $Config.BackupPath $backupFileName
# Source database path
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
try {
# Copy database file
Copy-Item -Path $dbPath -Destination $backupFilePath -Force
Write-Log "Database backed up to: $backupFileName" -Level "SUCCESS"
# Get backup file size
$backupSize = (Get-Item $backupFilePath).Length / 1KB
Write-Log "Backup size: $([math]::Round($backupSize, 2)) KB" -Level "INFO"
# Compress if enabled
if ($Config.Compress) {
$compressedPath = Compress-Backup -BackupPath $backupFilePath
if ($compressedPath) {
# Remove uncompressed backup
Remove-Item -Path $backupFilePath -Force
Write-Log "Uncompressed backup removed" -Level "INFO"
return $compressedPath
}
}
return $backupFilePath
} catch {
Write-Log "Backup failed: $_" -Level "ERROR"
return $null
}
}
function Compress-Backup {
param([string]$BackupPath)
Write-Log "Compressing backup..." -Level "INFO"
$zipPath = "$BackupPath.zip"
try {
# Create ZIP archive
Compress-Archive -Path $BackupPath -DestinationPath $zipPath -CompressionLevel Optimal -Force
# Get compressed size
$originalSize = (Get-Item $BackupPath).Length / 1KB
$compressedSize = (Get-Item $zipPath).Length / 1KB
$ratio = [math]::Round((1 - ($compressedSize / $originalSize)) * 100, 1)
Write-Log "Backup compressed: $([math]::Round($compressedSize, 2)) KB (saved $ratio%)" -Level "SUCCESS"
return $zipPath
} catch {
Write-Log "Compression failed: $_" -Level "WARN"
return $null
}
}
function Remove-OldBackups {
Write-Log "Cleaning up old backups (keeping last $($Config.RetentionDays) days)..." -Level "INFO"
$cutoffDate = (Get-Date).AddDays(-$Config.RetentionDays)
try {
# Get all backup files (both .db and .zip)
$allBackups = Get-ChildItem -Path $Config.BackupPath -File |
Where-Object {
$_.Name -like "telegram_bot_backup_*.db" -or
$_.Name -like "telegram_bot_backup_*.db.zip"
}
$removedCount = 0
$freedSpace = 0
foreach ($backup in $allBackups) {
if ($backup.LastWriteTime -lt $cutoffDate) {
$size = $backup.Length / 1MB
Remove-Item -Path $backup.FullName -Force
$removedCount++
$freedSpace += $size
Write-Log "Removed old backup: $($backup.Name)" -Level "INFO"
}
}
if ($removedCount -gt 0) {
Write-Log "Removed $removedCount old backup(s), freed $([math]::Round($freedSpace, 2)) MB" -Level "SUCCESS"
} else {
Write-Log "No old backups to remove" -Level "INFO"
}
} catch {
Write-Log "Failed to clean up old backups: $_" -Level "WARN"
}
}
function Get-BackupStatistics {
Write-Log "Backup Statistics:" -Level "INFO"
# Count backups
$allBackups = Get-ChildItem -Path $Config.BackupPath -File |
Where-Object {
$_.Name -like "telegram_bot_backup_*.db" -or
$_.Name -like "telegram_bot_backup_*.db.zip"
}
$totalBackups = $allBackups.Count
$totalSize = ($allBackups | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Log " Total backups: $totalBackups" -Level "INFO"
Write-Log " Total size: $([math]::Round($totalSize, 2)) MB" -Level "INFO"
# Latest backup
if ($totalBackups -gt 0) {
$latest = $allBackups | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Write-Log " Latest backup: $($latest.Name) ($([math]::Round($latest.Length / 1KB, 2)) KB)" -Level "INFO"
Write-Log " Created: $($latest.LastWriteTime)" -Level "INFO"
}
# Oldest backup
if ($totalBackups -gt 1) {
$oldest = $allBackups | Sort-Object LastWriteTime | Select-Object -First 1
$age = (Get-Date) - $oldest.LastWriteTime
Write-Log " Oldest backup: $($oldest.Name) (Age: $([math]::Round($age.TotalDays, 1)) days)" -Level "INFO"
}
}
function Test-BackupIntegrity {
param([string]$BackupPath)
Write-Log "Testing backup integrity..." -Level "INFO"
try {
# For ZIP files, test archive
if ($BackupPath -like "*.zip") {
# Try to extract to temp location
$tempExtract = Join-Path $env:TEMP "telegram_bot_backup_test"
if (Test-Path $tempExtract) {
Remove-Item -Path $tempExtract -Recurse -Force
}
Expand-Archive -Path $BackupPath -DestinationPath $tempExtract -Force
# Check if database file exists in extracted folder
$extractedDb = Get-ChildItem -Path $tempExtract -Filter "*.db" -Recurse
if ($extractedDb) {
Write-Log "Backup integrity test PASSED (ZIP archive valid)" -Level "SUCCESS"
Remove-Item -Path $tempExtract -Recurse -Force
return $true
} else {
Write-Log "Backup integrity test FAILED (No database file in archive)" -Level "ERROR"
Remove-Item -Path $tempExtract -Recurse -Force
return $false
}
} else {
# For .db files, try to open
$stream = [System.IO.File]::Open($BackupPath, 'Open', 'Read', 'Read')
$stream.Close()
Write-Log "Backup integrity test PASSED (Database file readable)" -Level "SUCCESS"
return $true
}
} catch {
Write-Log "Backup integrity test FAILED: $_" -Level "ERROR"
return $false
}
}
# =============================================================================
# MAIN BACKUP FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB Telegram Bot - Database Backup
SQLite database backup and retention management
====================================================================
"@ -ForegroundColor Cyan
Write-Log "Backup started by: $env:USERNAME" -Level "INFO"
Write-Log "Backup script: $($MyInvocation.MyCommand.Path)" -Level "INFO"
# Check if database exists
if (-not (Test-DatabaseFile)) {
Write-Log "Backup aborted: Database file not accessible" -Level "ERROR"
exit 1
}
# Get current database size
$dbSize = Get-DatabaseSize
Write-Log "Current database size: $dbSize KB" -Level "INFO"
try {
# Perform backup
$backupPath = Backup-Database
if ($backupPath) {
# Test backup integrity
$integrityOk = Test-BackupIntegrity -BackupPath $backupPath
if ($integrityOk) {
# Cleanup old backups
Remove-OldBackups
# Show statistics
Get-BackupStatistics
Write-Log "Backup completed successfully" -Level "SUCCESS"
exit 0
} else {
Write-Log "Backup created but failed integrity test" -Level "ERROR"
exit 1
}
} else {
Write-Log "Backup failed" -Level "ERROR"
exit 1
}
} catch {
Write-Log "Backup process failed: $_" -Level "ERROR"
Write-Log $_.ScriptStackTrace -Level "ERROR"
exit 1
}
}
# Run main backup
Main

View File

@@ -0,0 +1,585 @@
<#
.SYNOPSIS
Build ROA2WEB Frontend for Production Deployment
.DESCRIPTION
This script builds the Vue.js frontend for Windows Server deployment:
- Checks for Node.js installation
- Installs npm dependencies
- Builds production-optimized static files
- Creates deployment package with backend files
- Optionally transfers to remote server
.PARAMETER BackendSource
Path to backend source (default: ../../reports-app/backend)
.PARAMETER FrontendSource
Path to frontend source (default: ../../reports-app/frontend)
.PARAMETER OutputPath
Output path for deployment package (default: ./deploy-package)
.PARAMETER ServerPath
Remote server path for automatic deployment (optional)
.PARAMETER ServerHost
Remote server hostname/IP for automatic deployment (optional)
.EXAMPLE
.\Build-Frontend.ps1
Build with defaults, output to ./deploy-package
.EXAMPLE
.\Build-Frontend.ps1 -OutputPath "D:\deployments\roa2web-$(Get-Date -Format 'yyyyMMdd')"
Build to custom output path
.EXAMPLE
.\Build-Frontend.ps1 -ServerHost "10.0.20.170" -ServerPath "C:\Temp\roa2web-deploy"
Build and transfer to remote server
.NOTES
Author: ROA2WEB Team
Requires: Node.js 16+, npm
Can run on: WSL, Windows, Linux
#>
[CmdletBinding()]
param(
[string]$BackendSource = "../../reports-app/backend",
[string]$FrontendSource = "../../reports-app/frontend",
[string]$OutputPath = "./deploy-package",
[string]$ServerHost = "",
[string]$ServerPath = ""
)
$ErrorActionPreference = "Stop"
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-NodeJS {
Write-Step "Checking Node.js installation..."
try {
$nodeVersion = node --version 2>&1
$npmVersion = npm --version 2>&1
Write-Success "Node.js: $nodeVersion"
Write-Success "npm: $npmVersion"
# Check minimum version (16.x)
if ($nodeVersion -match "v(\d+)\.") {
$major = [int]$matches[1]
if ($major -lt 16) {
throw "Node.js version 16+ required (found: $nodeVersion)"
}
}
return $true
} catch {
Write-Error "Node.js not found or version too old"
Write-Host "`n Install Node.js from: https://nodejs.org/" -ForegroundColor Yellow
Write-Host " Minimum version: 16.x" -ForegroundColor Yellow
throw
}
}
function Resolve-FullPath {
param([string]$Path)
$scriptDir = Split-Path -Parent $PSScriptRoot
$fullPath = Join-Path $scriptDir $Path
# Convert to absolute path and resolve .. and . properly
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
return $fullPath
}
function Build-Frontend {
param([string]$SourcePath)
Write-Step "Building Vue.js frontend..."
if (-not (Test-Path $SourcePath)) {
throw "Frontend source path not found: $SourcePath"
}
Push-Location $SourcePath
try {
# Clean node_modules if it exists (to avoid EPERM errors)
$nodeModulesPath = Join-Path $SourcePath "node_modules"
if (Test-Path $nodeModulesPath) {
Write-Step "Cleaning existing node_modules..."
try {
Remove-Item -Path $nodeModulesPath -Recurse -Force -ErrorAction Stop
Write-Success "Cleaned node_modules"
} catch {
Write-Warning "Could not remove node_modules: $_"
Write-Warning "Please close VS Code/IDE and try again, or run as Administrator"
throw "Cannot proceed with locked node_modules. Close all IDEs and retry."
}
}
# Install dependencies
Write-Step "Installing npm dependencies (this may take a minute)..."
# Show npm output for transparency, redirect to host to prevent capture
npm install | Out-Default
# Verify node_modules was created
if (-not (Test-Path $nodeModulesPath)) {
throw "npm install failed: node_modules not created. Check errors above."
}
Write-Success "Dependencies installed"
# Build for production
Write-Step "Building for production (this may take a minute)..."
$env:NODE_ENV = "production"
# Show build output for transparency, redirect to host to prevent capture
npm run build | Out-Default
Write-Success "Build completed"
# Verify dist folder
$distPath = Join-Path $SourcePath "dist"
if (-not (Test-Path $distPath)) {
throw "Build failed: dist folder not found"
}
$distFiles = Get-ChildItem -Path $distPath -Recurse -File
$totalSize = ($distFiles | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Success "Generated $(($distFiles).Count) files (Total: $([math]::Round($totalSize, 2)) MB)"
return $distPath
} finally {
Pop-Location
}
}
function Copy-BackendFiles {
param(
[string]$SourcePath,
[string]$DestPath
)
Write-Step "Copying backend files..."
if (-not (Test-Path $SourcePath)) {
throw "Backend source path not found: $SourcePath"
}
# Ensure destination exists
if (-not (Test-Path $DestPath)) {
New-Item -ItemType Directory -Path $DestPath -Force | Out-Null
}
# Exclude directory names (will skip entire directory trees)
$excludeDirs = @(
"venv",
"__pycache__",
".pytest_cache",
"logs",
"temp",
"node_modules"
)
# Exclude file patterns
$excludeFiles = @(
"*.pyc",
"*.pyo",
"*.log",
".env",
".env.local"
)
# Normalize source path (ensure trailing backslash for proper substring calculation)
$normalizedSourcePath = $SourcePath.TrimEnd('\', '/') + '\'
# Helper function to check if path should be excluded (using script: scope to access parent variables)
$testExclude = {
param([string]$RelativePath, [bool]$IsDirectory, [array]$ExcludeDirs, [array]$ExcludeFiles)
# Check directory exclusions (match directory name exactly)
if ($IsDirectory) {
$dirName = Split-Path $RelativePath -Leaf
if ($ExcludeDirs -contains $dirName) {
return $true
}
}
# Check if any parent directory should be excluded
$pathParts = $RelativePath -split '[\\/]'
foreach ($part in $pathParts) {
if ($ExcludeDirs -contains $part) {
return $true
}
}
# Check file patterns
if (-not $IsDirectory) {
foreach ($pattern in $ExcludeFiles) {
if ($RelativePath -like $pattern) {
return $true
}
}
}
return $false
}
# Copy files
Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object {
# Calculate relative path safely
if ($_.FullName.Length -le $normalizedSourcePath.Length) {
return # Skip if path is too short (shouldn't happen, but safety check)
}
$relativePath = $_.FullName.Substring($normalizedSourcePath.Length)
# Check if should be excluded
$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) backend files"
}
function New-DeploymentPackage {
param(
[string]$FrontendDistPath,
[string]$BackendSourcePath,
[string]$OutputPath
)
Write-Step "Creating deployment package..."
# Create output directory
if (Test-Path $OutputPath) {
Write-Warning "Output path exists, cleaning..."
Remove-Item -Path $OutputPath -Recurse -Force
}
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
# Create structure
$frontendDest = Join-Path $OutputPath "frontend"
$backendDest = Join-Path $OutputPath "backend"
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
New-Item -ItemType Directory -Path $backendDest -Force | Out-Null
# Copy frontend dist
Write-Step "Copying frontend files..."
Copy-Item -Path "$FrontendDistPath\*" -Destination $frontendDest -Recurse -Force
Write-Success "Frontend files copied"
# Copy backend files
Copy-BackendFiles -SourcePath $BackendSourcePath -DestPath $backendDest
# Copy shared modules (database, auth, utils)
Write-Step "Copying shared modules..."
$sharedSource = Join-Path (Split-Path (Split-Path $BackendSourcePath -Parent) -Parent) "shared"
$sharedDest = Join-Path $OutputPath "shared"
if (Test-Path $sharedSource) {
Copy-Item -Path $sharedSource -Destination $sharedDest -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests")
Write-Success "Shared modules copied"
} else {
Write-Warning "Shared modules not found at: $sharedSource"
}
# Copy deployment config
$configSource = Join-Path (Split-Path -Parent $PSScriptRoot) "config"
$configDest = Join-Path $OutputPath "config"
if (Test-Path $configSource) {
Copy-Item -Path $configSource -Destination $configDest -Recurse -Force
Write-Success "Config files copied"
}
# Copy deployment scripts
Write-Step "Copying deployment scripts..."
$scriptsSource = $PSScriptRoot
$scriptsDest = Join-Path $OutputPath "scripts"
New-Item -ItemType Directory -Path $scriptsDest -Force | Out-Null
# List of scripts to include in deployment package
$deploymentScripts = @(
"Install-ROA2WEB.ps1",
"Deploy-ROA2WEB.ps1",
"Start-ROA2WEB.ps1",
"Stop-ROA2WEB.ps1",
"Restart-ROA2WEB.ps1"
)
$copiedScripts = 0
foreach ($script in $deploymentScripts) {
$scriptPath = Join-Path $scriptsSource $script
if (Test-Path $scriptPath) {
Copy-Item -Path $scriptPath -Destination $scriptsDest -Force
$copiedScripts++
}
}
Write-Success "Copied $copiedScripts deployment scripts"
# Create README
$readmePath = Join-Path $OutputPath "README.txt"
$readme = @"
================================================================================
ROA2WEB DEPLOYMENT PACKAGE
Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
================================================================================
CONTENTS:
---------
backend/ FastAPI backend application files (Python)
frontend/ Vue.js static files (built for production)
config/ IIS configuration files (.env template, web.config)
scripts/ PowerShell management scripts
DEPLOYMENT SCRIPTS:
-------------------
Install-ROA2WEB.ps1 First-time setup (Python venv, IIS site)
Deploy-ROA2WEB.ps1 Update application files (auto-detects source)
Start-ROA2WEB.ps1 Start backend service + IIS
Stop-ROA2WEB.ps1 Stop backend service + IIS
Restart-ROA2WEB.ps1 Quick restart
================================================================================
DEPLOYMENT WORKFLOW
================================================================================
>> FIRST TIME INSTALLATION:
---------------------------
1. Copy this entire folder to server (e.g., C:\Deploy\ROA2WEB-v1)
2. Open PowerShell as Administrator:
cd C:\Deploy\ROA2WEB-v1\scripts
.\Install-ROA2WEB.ps1
3. Configure environment:
notepad C:\inetpub\wwwroot\roa2web\backend\.env
4. Start services:
.\Start-ROA2WEB.ps1
5. Access: http://localhost:8080
>> UPDATES (New code version):
-------------------------------
1. Copy new deployment package to server (e.g., C:\Deploy\ROA2WEB-v2)
2. Open PowerShell as Administrator:
cd C:\Deploy\ROA2WEB-v2\scripts
3. Deploy (automatically stops, updates, and starts):
.\Stop-ROA2WEB.ps1
.\Deploy-ROA2WEB.ps1
.\Start-ROA2WEB.ps1
Note: Deploy-ROA2WEB.ps1 auto-detects source path (no parameters needed!)
4. Done! Application updated with new code.
>> QUICK OPERATIONS:
--------------------
Restart app: .\Restart-ROA2WEB.ps1
Stop app: .\Stop-ROA2WEB.ps1
Start app: .\Start-ROA2WEB.ps1
================================================================================
REQUIREMENTS
================================================================================
- Windows Server 2016+ or Windows 10/11
- IIS already installed (with ASP.NET Core Hosting Bundle)
- Python 3.8+ installed
- PowerShell 5.1+ (run as Administrator)
NOTES:
------
Backend files do NOT include venv (virtual environment)
Install-ROA2WEB.ps1 creates venv and installs dependencies automatically
Deploy-ROA2WEB.ps1 creates backup before updating
.env files are preserved during updates
Application installs to: C:\inetpub\wwwroot\roa2web\
TROUBLESHOOTING:
----------------
Check logs: C:\inetpub\wwwroot\roa2web\logs\
Backend logs: C:\inetpub\wwwroot\roa2web\backend\backend.log
IIS logs: C:\inetpub\logs\LogFiles\
For detailed documentation, see: WINDOWS_DEPLOYMENT.md
================================================================================
"@
Set-Content -Path $readmePath -Value $readme -Force
# Calculate package size
$packageFiles = Get-ChildItem -Path $OutputPath -Recurse -File
$packageSize = ($packageFiles | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Success "Deployment package created: $OutputPath"
Write-Success "Total files: $(($packageFiles).Count)"
Write-Success "Total size: $([math]::Round($packageSize, 2)) MB"
return $OutputPath
}
function Copy-ToRemoteServer {
param(
[string]$LocalPath,
[string]$ServerHost,
[string]$ServerPath
)
Write-Step "Transferring to remote server..."
try {
# Check if remote server is accessible
$pingResult = Test-Connection -ComputerName $ServerHost -Count 1 -Quiet
if (-not $pingResult) {
Write-Warning "Server $ServerHost not reachable"
return $false
}
# Use robocopy for efficient transfer (Windows)
if ($IsWindows -or $env:OS -match "Windows") {
$remotePath = "\\$ServerHost\$($ServerPath -replace ':', '$')"
Write-Host " [*] Copying to: $remotePath" -ForegroundColor Yellow
robocopy $LocalPath $remotePath /E /Z /R:3 /W:5 /MT:8 /NFL /NDL /NP
if ($LASTEXITCODE -le 7) {
Write-Success "Files transferred successfully"
return $true
} else {
Write-Error "Transfer failed with code: $LASTEXITCODE"
return $false
}
} else {
# Use scp for Unix/WSL
Write-Warning "Remote copy via SCP not yet implemented"
Write-Host " Manual transfer required to: $ServerHost`:$ServerPath" -ForegroundColor Yellow
return $false
}
} catch {
Write-Error "Failed to transfer to server: $_"
return $false
}
}
# =============================================================================
# MAIN BUILD FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB - Frontend Build Script
Build Vue.js frontend and create deployment package
====================================================================
"@ -ForegroundColor Cyan
try {
# Resolve paths
$backendSourcePath = Resolve-FullPath -Path $BackendSource
$frontendSourcePath = Resolve-FullPath -Path $FrontendSource
$outputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Resolve-FullPath -Path $OutputPath }
Write-Host "`nPaths:" -ForegroundColor Yellow
Write-Host " Backend Source: $backendSourcePath"
Write-Host " Frontend Source: $frontendSourcePath"
Write-Host " Output Path: $outputPath"
# Check Node.js
Test-NodeJS
# Build frontend
$distPath = Build-Frontend -SourcePath $frontendSourcePath
# Create deployment package
$packagePath = New-DeploymentPackage `
-FrontendDistPath $distPath `
-BackendSourcePath $backendSourcePath `
-OutputPath $outputPath
# Transfer to server if specified
if ($ServerHost -and $ServerPath) {
$transferred = Copy-ToRemoteServer `
-LocalPath $packagePath `
-ServerHost $ServerHost `
-ServerPath $ServerPath
}
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " BUILD COMPLETED SUCCESSFULLY" -ForegroundColor Green
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host "`nDeployment Package: $packagePath" -ForegroundColor Yellow
if ($ServerHost) {
Write-Host "`nNext Steps (on server $ServerHost):" -ForegroundColor Yellow
Write-Host " cd $ServerPath"
Write-Host " .\Deploy-ROA2WEB.ps1 -SourcePath ."
} else {
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host " 1. Transfer '$packagePath' to your Windows Server"
Write-Host " 2. On the server, run: Deploy-ROA2WEB.ps1 -SourcePath '<transferred-path>'"
}
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
} catch {
Write-Host "`n[FATAL ERROR] Build failed: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
exit 1
}
}
# Run main build
Main

View File

@@ -0,0 +1,788 @@
<#
.SYNOPSIS
Build ROA2WEB Telegram Bot for Windows Server Deployment
.DESCRIPTION
This script creates a deployment package for the Telegram bot:
- Copies application source files (app/)
- Copies requirements.txt
- Copies PowerShell deployment scripts
- Creates .env.example template
- Creates deployment README
- Excludes development files (venv, data, logs, etc.)
- Optionally transfers to remote server
.PARAMETER SourcePath
Path to telegram-bot source (default: ../../reports-app/telegram-bot)
.PARAMETER OutputPath
Output path for deployment package (default: ../deploy-package/telegram-bot)
.PARAMETER ServerHost
Remote server hostname/IP for automatic deployment (optional)
.PARAMETER ServerPath
Remote server path for automatic deployment (optional)
.PARAMETER Clean
Clean output directory before building (default: true)
.EXAMPLE
.\Build-TelegramBot.ps1
Build with defaults
.EXAMPLE
.\Build-TelegramBot.ps1 -OutputPath "D:\deployments\telegram-bot-$(Get-Date -Format 'yyyyMMdd')"
Build to custom output path
.EXAMPLE
.\Build-TelegramBot.ps1 -ServerHost "10.0.20.36" -ServerPath "C:\Temp\telegram-bot-deploy"
Build and transfer to remote server
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+
Can run on: WSL, Windows, Linux
#>
[CmdletBinding()]
param(
[string]$SourcePath = "../../../reports-app/telegram-bot",
[string]$OutputPath = "../deploy-package/telegram-bot",
[string]$ServerHost = "",
[string]$ServerPath = "",
[bool]$Clean = $true
)
$ErrorActionPreference = "Stop"
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Resolve-FullPath {
param([string]$Path)
$scriptDir = $PSScriptRoot
$fullPath = Join-Path $scriptDir $Path
# Convert to absolute path and resolve .. and . properly
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
return $fullPath
}
function Test-SourceDirectory {
param([string]$Path)
Write-Step "Validating source directory..."
if (-not (Test-Path $Path)) {
throw "Source path not found: $Path"
}
# Check for required files/directories
$requiredPaths = @(
(Join-Path $Path "app"),
(Join-Path $Path "requirements.txt")
)
foreach ($reqPath in $requiredPaths) {
if (-not (Test-Path $reqPath)) {
throw "Required path not found: $reqPath"
}
}
Write-Success "Source directory validated: $Path"
}
function New-CleanOutputDirectory {
param([string]$Path)
if ($Clean -and (Test-Path $Path)) {
Write-Step "Cleaning output directory..."
Remove-Item -Path $Path -Recurse -Force
Write-Success "Output directory cleaned"
}
if (-not (Test-Path $Path)) {
New-Item -ItemType Directory -Path $Path -Force | Out-Null
Write-Success "Created output directory: $Path"
}
}
function Copy-ApplicationFiles {
param(
[string]$SourcePath,
[string]$DestPath
)
Write-Step "Copying application files..."
# Exclude patterns
$excludeDirs = @(
"venv",
"data",
"logs",
"temp",
"backups",
"__pycache__",
".pytest_cache",
"tests",
".git"
)
$excludeFiles = @(
".env",
"*.pyc",
"*.pyo",
"*.log",
"*.db",
".DS_Store",
"Thumbs.db"
)
# Create app directory in destination
$destApp = Join-Path $DestPath "app"
New-Item -ItemType Directory -Path $destApp -Force | Out-Null
# Copy app/ directory
$sourceApp = Join-Path $SourcePath "app"
$fileCount = 0
Get-ChildItem -Path $sourceApp -Recurse | ForEach-Object {
# Check if in excluded directory
$inExcludedDir = $false
foreach ($excludeDir in $excludeDirs) {
if ($_.FullName -like "*\$excludeDir\*" -or $_.FullName -like "*/$excludeDir/*") {
$inExcludedDir = $true
break
}
}
if ($inExcludedDir) {
return
}
# Check if excluded file
$isExcludedFile = $false
foreach ($pattern in $excludeFiles) {
if ($_.Name -like $pattern) {
$isExcludedFile = $true
break
}
}
if ($isExcludedFile) {
return
}
# Calculate relative path and destination
$relativePath = $_.FullName.Substring($sourceApp.Length)
$destFile = Join-Path $destApp $relativePath
if ($_.PSIsContainer) {
# Create directory
if (-not (Test-Path $destFile)) {
New-Item -ItemType Directory -Path $destFile -Force | Out-Null
}
} else {
# Copy file
$destFileDir = Split-Path $destFile -Parent
if (-not (Test-Path $destFileDir)) {
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
}
Copy-Item -Path $_.FullName -Destination $destFile -Force
$fileCount++
}
}
Write-Success "Copied $fileCount application files"
}
function Copy-RequirementsFile {
param(
[string]$SourcePath,
[string]$DestPath
)
Write-Step "Copying requirements.txt..."
$sourceReq = Join-Path $SourcePath "requirements.txt"
$destReq = Join-Path $DestPath "requirements.txt"
if (Test-Path $sourceReq) {
Copy-Item -Path $sourceReq -Destination $destReq -Force
Write-Success "requirements.txt copied"
} else {
Write-Warning "requirements.txt not found in source"
}
}
function New-EnvironmentTemplate {
param([string]$DestPath)
Write-Step "Creating .env.example template..."
$envTemplate = @"
# ROA2WEB Telegram Bot - Production Configuration Template
# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_production_bot_token_from_@BotFather
# Claude API Configuration
CLAUDE_API_KEY=your_production_claude_api_key_from_anthropic_console
# Backend API Configuration
BACKEND_URL=http://localhost:8000
BACKEND_TIMEOUT=30
# SQLite Database Configuration
SQLITE_DB_PATH=C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
# Internal API Configuration (for backend callbacks)
INTERNAL_API_HOST=127.0.0.1
INTERNAL_API_PORT=8002
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=C:\inetpub\wwwroot\roa2web\telegram-bot\logs\bot.log
# Environment
ENVIRONMENT=production
# Authentication Configuration
AUTH_CODE_EXPIRY_MINUTES=15
JWT_REFRESH_THRESHOLD_MINUTES=5
# Session Configuration
SESSION_TIMEOUT_MINUTES=60
MAX_CONVERSATION_HISTORY=20
"@
$envPath = Join-Path $DestPath ".env.example"
Set-Content -Path $envPath -Value $envTemplate -Encoding UTF8
Write-Success ".env.example template created"
}
function Copy-DeploymentScripts {
param(
[string]$SourceScriptsPath,
[string]$DestPath
)
Write-Step "Copying deployment scripts..."
$scriptsDir = Join-Path $DestPath "scripts"
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
# List of scripts to copy
$scripts = @(
"Install-TelegramBot.ps1",
"Deploy-TelegramBot.ps1",
"Start-TelegramBot.ps1",
"Stop-TelegramBot.ps1",
"Restart-TelegramBot.ps1",
"Backup-TelegramDB.ps1",
"Setup-DailyBackup.ps1",
"Setup-ClaudeAuth.ps1"
)
$copiedCount = 0
foreach ($script in $scripts) {
$sourcePath = Join-Path $SourceScriptsPath $script
if (Test-Path $sourcePath) {
$destScript = Join-Path $scriptsDir $script
Copy-Item -Path $sourcePath -Destination $destScript -Force
$copiedCount++
} else {
Write-Warning "Script not found: $script"
}
}
Write-Success "Copied $copiedCount deployment scripts"
}
function Copy-ConfigTemplates {
param(
[string]$SourceConfigPath,
[string]$DestPath
)
Write-Step "Copying configuration templates..."
$configDir = Join-Path $DestPath "config"
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
# Copy .env.production.windows.telegram if it exists
$prodEnv = Join-Path $SourceConfigPath ".env.production.windows.telegram"
if (Test-Path $prodEnv) {
Copy-Item -Path $prodEnv -Destination (Join-Path $configDir ".env.production.windows.telegram") -Force
Write-Success "Production config template copied"
}
}
function New-DeploymentReadme {
param([string]$DestPath)
Write-Step "Creating deployment README..."
$readme = @"
# ROA2WEB Telegram Bot - Deployment Package
**Created**: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
**Version**: Production Deployment Package
## Contents
- ``app/`` - Telegram bot application source code
- ``scripts/`` - PowerShell deployment and management scripts
- ``config/`` - Configuration templates
- ``requirements.txt`` - Python dependencies
- ``.env.example`` - Environment configuration template
- ``README.txt`` - This file
## Deployment Instructions
### 1. Initial Installation
Run as Administrator on Windows Server:
```powershell
cd scripts
.\Install-TelegramBot.ps1
```
This will:
- Check Python 3.11+ installation
- Install NSSM (service manager)
- Create directory structure
- Create virtual environment
- Install Python dependencies
- Create Windows Service (ROA2WEB-TelegramBot)
- Create configuration template
### 2. Configuration
Edit the ``.env`` file:
```powershell
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
```
Required settings:
- ``TELEGRAM_BOT_TOKEN`` - Get from @BotFather on Telegram
- ``CLAUDE_API_KEY`` - Get from Anthropic console
- ``BACKEND_URL`` - Usually http://localhost:8000
### 3. Start Service
```powershell
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
.\Start-TelegramBot.ps1
```
### 4. Verify Deployment
Check health endpoint:
```powershell
Invoke-WebRequest http://localhost:8002/internal/health
```
View logs:
```powershell
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log -Tail 50 -Wait
```
## Management Commands
- **Start**: ``.\Start-TelegramBot.ps1``
- **Stop**: ``.\Stop-TelegramBot.ps1``
- **Restart**: ``.\Restart-TelegramBot.ps1``
- **Deploy Update**: ``.\Deploy-TelegramBot.ps1 -SourcePath "path\to\new\package"``
- **Backup Database**: ``.\Backup-TelegramDB.ps1``
- **Setup Daily Backup**: ``.\Setup-DailyBackup.ps1``
## Directory Structure
```
C:\inetpub\wwwroot\roa2web\telegram-bot\
app\ # Application source code
venv\ # Python virtual environment
data\ # SQLite database (telegram_bot.db)
logs\ # Application logs
backups\ # Database backups
temp\ # Temporary files
scripts\ # Management scripts
config\ # Configuration templates
requirements.txt # Python dependencies
.env # Environment configuration
```
## Troubleshooting
### Service won't start
Check logs:
```powershell
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
```
### Bot not responding on Telegram
1. Verify service is running: ``Get-Service ROA2WEB-TelegramBot``
2. Check health endpoint: ``Invoke-WebRequest http://localhost:8002/internal/health``
3. Verify ``.env`` configuration (TELEGRAM_BOT_TOKEN)
4. Check logs for errors
### Database errors
Run database backup and check integrity:
```powershell
.\Backup-TelegramDB.ps1
```
## Support
- Documentation: ``C:\inetpub\wwwroot\roa2web\deployment\windows\docs\TELEGRAM_BOT_DEPLOYMENT.md``
- Project repository: ROA2WEB on GitHub
- Contact: ROA2WEB Team
"@
$readmePath = Join-Path $DestPath "README.txt"
Set-Content -Path $readmePath -Value $readme -Encoding UTF8
Write-Success "Deployment README created"
}
function Copy-ClaudeCredentials {
param([string]$PackagePath)
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow
Write-Host " OPTIONAL: Claude Credentials" -ForegroundColor Yellow
Write-Host ("=" * 60) -ForegroundColor Yellow
Write-Host ""
Write-Host "If you have Claude Pro/Max credentials from 'claude-code login',"
Write-Host "you can include them in the deployment package for easy setup."
Write-Host ""
$response = Read-Host "Copy Claude credentials to deployment package? (Y/N)"
if ($response -eq "Y" -or $response -eq "y") {
# Try to find credentials automatically in both possible locations
$possiblePaths = @(
(Join-Path $env:USERPROFILE ".claude\.credentials.json"), # Correct location
(Join-Path $env:APPDATA "claude\credentials.json") # Alternative location
)
$defaultCredPath = $null
foreach ($path in $possiblePaths) {
if (Test-Path $path) {
$defaultCredPath = $path
break
}
}
if ($defaultCredPath) {
Write-Host "`nFound credentials at: $defaultCredPath" -ForegroundColor Green
$usePath = Read-Host "Use this path? (Y/N)"
if ($usePath -eq "Y" -or $usePath -eq "y") {
$credPath = $defaultCredPath
} else {
$credPath = Read-Host "Enter path to credentials.json"
}
} else {
Write-Host "`nCredentials not found at default locations" -ForegroundColor Yellow
Write-Host " Checked: $($possiblePaths -join ', ')" -ForegroundColor Gray
$credPath = Read-Host "Enter full path to credentials.json"
}
if (Test-Path $credPath) {
try {
$destCredPath = Join-Path $PackagePath "claude-credentials.json"
Copy-Item -Path $credPath -Destination $destCredPath -Force
Write-Success "Claude credentials copied to deployment package"
Write-Host " Location: $destCredPath" -ForegroundColor Gray
Write-Host " The Setup-ClaudeAuth.ps1 script will detect and use this file automatically" -ForegroundColor Gray
return $true
} catch {
Write-Warning "Failed to copy credentials: $_"
return $false
}
} else {
Write-Warning "Credentials file not found at: $credPath"
return $false
}
} else {
Write-Host "Skipping credentials copy. You can set up Claude auth manually on the server." -ForegroundColor Gray
return $false
}
}
function Transfer-ToServerSSH {
param([string]$PackagePath)
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow
Write-Host " OPTIONAL: SSH Transfer to Server" -ForegroundColor Yellow
Write-Host ("=" * 60) -ForegroundColor Yellow
Write-Host ""
Write-Host "You can automatically transfer the deployment package to"
Write-Host "the Windows Server via SSH/SCP (requires SSH server on Windows)."
Write-Host ""
$response = Read-Host "Transfer package to server via SSH? (Y/N)"
if ($response -eq "Y" -or $response -eq "y") {
Write-Host ""
$sshUser = Read-Host "Enter SSH username (e.g., Administrator)"
$sshHost = Read-Host "Enter server hostname/IP (e.g., 10.0.20.36)"
$sshPort = Read-Host "Enter SSH port (default: 22, press Enter for default)"
if ([string]::IsNullOrWhiteSpace($sshPort)) {
$sshPort = "22"
}
$remotePath = Read-Host "Enter remote path (e.g., C:/Temp/telegram-bot-deploy)"
# Convert Windows path to SCP format if needed
$remotePath = $remotePath -replace '\\', '/'
if ($remotePath -match '^[A-Za-z]:') {
# Convert C:/path to /c/path format for SCP
$remotePath = $remotePath -replace '^([A-Za-z]):', '/$1'
}
Write-Host "`nTransfer Configuration:" -ForegroundColor Cyan
Write-Host " Source: $PackagePath"
Write-Host " Target: ${sshUser}@${sshHost}:${remotePath}"
Write-Host " Port: $sshPort"
Write-Host ""
$confirm = Read-Host "Proceed with transfer? (Y/N)"
if ($confirm -eq "Y" -or $confirm -eq "y") {
Write-Step "Transferring package via SCP..."
try {
# Use SCP to transfer
$scpTarget = "${sshUser}@${sshHost}:${remotePath}"
# Build SCP command
$scpArgs = @(
"-P", $sshPort,
"-r",
$PackagePath,
$scpTarget
)
Write-Host " Running: scp $scpArgs" -ForegroundColor Gray
& scp @scpArgs
if ($LASTEXITCODE -eq 0) {
Write-Success "Package transferred successfully!"
Write-Host " Remote location: $scpTarget" -ForegroundColor Gray
return $true
} else {
Write-Warning "SCP transfer failed with exit code: $LASTEXITCODE"
Write-Host " You can transfer manually via RDP or network share" -ForegroundColor Yellow
return $false
}
} catch {
Write-Warning "Transfer failed: $_"
Write-Host " Make sure 'scp' is available in PATH" -ForegroundColor Yellow
Write-Host " Alternative: Use WinSCP, FileZilla, or manual RDP copy" -ForegroundColor Yellow
return $false
}
} else {
Write-Host "Transfer cancelled" -ForegroundColor Gray
return $false
}
} else {
Write-Host "Skipping SSH transfer. Transfer package manually via RDP or network share." -ForegroundColor Gray
return $false
}
}
function Show-PackageSummary {
param(
[string]$PackagePath,
[bool]$CredentialsCopied,
[bool]$TransferredToServer
)
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host " DEPLOYMENT PACKAGE CREATED SUCCESSFULLY" -ForegroundColor Green
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nPackage Location:" -ForegroundColor Yellow
Write-Host " $PackagePath"
# Calculate package size
$files = Get-ChildItem -Path $PackagePath -Recurse -File
$totalSize = ($files | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`nPackage Contents:" -ForegroundColor Yellow
Write-Host " Files: $($files.Count)"
Write-Host " Total Size: $([math]::Round($totalSize, 2)) MB"
if ($CredentialsCopied) {
Write-Host " ✓ Claude credentials included" -ForegroundColor Green
}
if ($TransferredToServer) {
Write-Host "`nDeployment Status:" -ForegroundColor Yellow
Write-Host " ✓ Package transferred to server via SSH" -ForegroundColor Green
Write-Host ""
Write-Host "Next Steps on Server:" -ForegroundColor Yellow
Write-Host " 1. SSH to server or RDP"
Write-Host " 2. Navigate to deployment location"
Write-Host " 3. Run: scripts\Install-TelegramBot.ps1 (as Administrator)"
Write-Host " 4. Run: scripts\Setup-ClaudeAuth.ps1 (will auto-detect credentials)"
Write-Host " 5. Configure: .env file with Telegram bot token"
Write-Host " 6. Run: scripts\Start-TelegramBot.ps1"
} else {
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host " 1. Transfer package to Windows Server (10.0.20.36)"
Write-Host " - Via network share: Copy-Item -Path $PackagePath -Destination \\10.0.20.36\C$\Temp\telegram-bot-deploy -Recurse"
Write-Host " - Via RDP: Manual copy"
Write-Host " 2. On server, run: scripts\Install-TelegramBot.ps1 (as Administrator)"
Write-Host " 3. Configure: .env file with production credentials"
Write-Host " 4. Run: scripts\Start-TelegramBot.ps1"
}
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
function Transfer-ToServer {
param(
[string]$PackagePath,
[string]$ServerHost,
[string]$ServerPath
)
if (-not $ServerHost -or -not $ServerPath) {
return
}
Write-Step "Transferring to server $ServerHost..."
try {
# Check if server is reachable
if (Test-Connection -ComputerName $ServerHost -Count 1 -Quiet) {
Write-Success "Server is reachable"
} else {
Write-Warning "Server is not reachable, skipping transfer"
return
}
# Transfer files (using Copy-Item for network path or RoboCopy)
$networkPath = "\\$ServerHost\$(($ServerPath -replace ':', '$'))"
Write-Step "Copying to: $networkPath"
# Ensure destination exists
if (-not (Test-Path $networkPath)) {
New-Item -ItemType Directory -Path $networkPath -Force | Out-Null
}
# Copy package
Copy-Item -Path "$PackagePath\*" -Destination $networkPath -Recurse -Force
Write-Success "Package transferred to server"
} catch {
Write-Warning "Failed to transfer to server: $_"
Write-Host " You can manually copy the package from: $PackagePath" -ForegroundColor Yellow
}
}
# =============================================================================
# MAIN BUILD FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB Telegram Bot - Build Deployment Package
Creating production-ready deployment package
====================================================================
"@ -ForegroundColor Cyan
# Resolve paths
$sourcePath = Resolve-FullPath -Path $SourcePath
$outputPath = Resolve-FullPath -Path $OutputPath
$scriptsPath = $PSScriptRoot
Write-Step "Build Configuration"
Write-Host " Source: $sourcePath" -ForegroundColor Gray
Write-Host " Output: $outputPath" -ForegroundColor Gray
try {
# Build steps
Test-SourceDirectory -Path $sourcePath
New-CleanOutputDirectory -Path $outputPath
Copy-ApplicationFiles -SourcePath $sourcePath -DestPath $outputPath
Copy-RequirementsFile -SourcePath $sourcePath -DestPath $outputPath
New-EnvironmentTemplate -DestPath $outputPath
Copy-DeploymentScripts -SourceScriptsPath $scriptsPath -DestPath $outputPath
# Copy config templates if they exist
$configPath = Join-Path (Split-Path $scriptsPath -Parent) "config"
if (Test-Path $configPath) {
Copy-ConfigTemplates -SourceConfigPath $configPath -DestPath $outputPath
}
New-DeploymentReadme -DestPath $outputPath
# Interactive: Copy Claude credentials to package
$credentialsCopied = Copy-ClaudeCredentials -PackagePath $outputPath
# Interactive: Transfer to server via SSH
$transferredToServer = $false
if (-not ($ServerHost -and $ServerPath)) {
# Interactive SSH transfer
$transferredToServer = Transfer-ToServerSSH -PackagePath $outputPath
} else {
# Legacy non-interactive transfer (if parameters provided)
Transfer-ToServer -PackagePath $outputPath -ServerHost $ServerHost -ServerPath $ServerPath
$transferredToServer = $true
}
# Show summary with deployment status
Show-PackageSummary -PackagePath $outputPath -CredentialsCopied $credentialsCopied -TransferredToServer $transferredToServer
Write-Host "`nBuild completed successfully!" -ForegroundColor Green
} catch {
Write-Host "`n[BUILD FAILED] $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
exit 1
}
}
# Run main build
Main

View File

@@ -0,0 +1,496 @@
<#
.SYNOPSIS
ROA2WEB - Quick Deployment/Update Script for Windows Server
.DESCRIPTION
This script performs rapid deployment or updates of ROA2WEB application:
- Auto-detects source path (use from scripts/ directory)
- Creates backup of current deployment
- Stops backend service
- Updates backend and/or frontend files
- Installs new Python dependencies if changed
- Restarts backend service
- Validates deployment health
.PARAMETER InstallPath
Target installation path (default: C:\inetpub\wwwroot\roa2web)
.PARAMETER BackupEnabled
Create backup before deployment (default: true)
.PARAMETER RestartService
Restart backend service after deployment (default: true)
.PARAMETER UpdateBackend
Update backend files (default: true)
.PARAMETER UpdateFrontend
Update frontend files (default: true)
.EXAMPLE
cd C:\Deploy\ROA2WEB\scripts
.\Deploy-ROA2WEB.ps1
Deploy from current deployment package (auto-detected)
.EXAMPLE
.\Deploy-ROA2WEB.ps1 -UpdateBackend -UpdateFrontend:$false
Update only backend files
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges
Must be run from deployment package's scripts/ directory
#>
[CmdletBinding()]
param(
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
[bool]$BackupEnabled = $true,
[bool]$RestartService = $true,
[bool]$UpdateBackend = $true,
[bool]$UpdateFrontend = $true
)
$ErrorActionPreference = "Stop"
# =============================================================================
# CONFIGURATION
# =============================================================================
# Auto-detect source path: if running from scripts/ subdirectory, use parent
$detectedSourcePath = $PSScriptRoot
if ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
$detectedSourcePath = Split-Path $PSScriptRoot -Parent
}
$script:Config = @{
AppName = "ROA2WEB"
ServiceName = "ROA2WEB-Backend"
InstallPath = $InstallPath
BackendPath = Join-Path $InstallPath "backend"
FrontendPath = Join-Path $InstallPath "frontend"
BackupPath = Join-Path $InstallPath "backups"
LogsPath = Join-Path $InstallPath "logs"
SourcePath = $detectedSourcePath
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function New-BackupDirectory {
if (-not (Test-Path $Config.BackupPath)) {
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
}
}
function Backup-CurrentDeployment {
if (-not $BackupEnabled) {
Write-Warning "Backup disabled, skipping..."
return $null
}
Write-Step "Creating backup of current deployment..."
New-BackupDirectory
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupName = "backup-$timestamp"
$backupFullPath = Join-Path $Config.BackupPath $backupName
try {
# Create backup directory
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
# Backup backend
if ((Test-Path $Config.BackendPath) -and $UpdateBackend) {
$backupBackendPath = Join-Path $backupFullPath "backend"
Copy-Item -Path $Config.BackendPath -Destination $backupBackendPath -Recurse -Force -ErrorAction SilentlyContinue
Write-Success "Backend backed up"
}
# Backup frontend
if ((Test-Path $Config.FrontendPath) -and $UpdateFrontend) {
$backupFrontendPath = Join-Path $backupFullPath "frontend"
Copy-Item -Path $Config.FrontendPath -Destination $backupFrontendPath -Recurse -Force -ErrorAction SilentlyContinue
Write-Success "Frontend backed up"
}
Write-Success "Backup created at: $backupFullPath"
# Clean old backups (keep last 10)
$allBackups = Get-ChildItem -Path $Config.BackupPath -Directory | Sort-Object Name -Descending
if ($allBackups.Count -gt 10) {
$oldBackups = $allBackups | Select-Object -Skip 10
foreach ($oldBackup in $oldBackups) {
Remove-Item -Path $oldBackup.FullName -Recurse -Force
Write-Success "Cleaned old backup: $($oldBackup.Name)"
}
}
return $backupFullPath
} catch {
Write-Error "Backup failed: $_"
throw
}
}
function Stop-BackendService {
Write-Step "Stopping backend service..."
try {
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Warning "Service $($Config.ServiceName) not found"
return
}
if ($service.Status -eq "Running") {
Stop-Service -Name $Config.ServiceName -Force
Start-Sleep -Seconds 2
# Wait for service to stop
$timeout = 30
$elapsed = 0
while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
}
if ($service.Status -eq "Stopped") {
Write-Success "Service stopped successfully"
} else {
Write-Warning "Service did not stop within timeout"
}
} else {
Write-Success "Service already stopped"
}
} catch {
Write-Error "Failed to stop service: $_"
throw
}
}
function Update-BackendFiles {
if (-not $UpdateBackend) {
Write-Warning "Backend update disabled, skipping..."
return
}
Write-Step "Updating backend files..."
$sourceBackend = Join-Path $Config.SourcePath "backend"
if (-not (Test-Path $sourceBackend)) {
Write-Warning "Source backend path not found: $sourceBackend"
return
}
try {
# Copy all files except .env (preserve existing config)
$excludeFiles = @("*.env", "*.log", "*.pyc", "__pycache__")
Get-ChildItem -Path $sourceBackend -Recurse -File | ForEach-Object {
$relativePath = $_.FullName.Substring($sourceBackend.Length)
$destPath = Join-Path $Config.BackendPath $relativePath
# Skip excluded files
$skip = $false
foreach ($pattern in $excludeFiles) {
if ($_.Name -like $pattern -or $_.Directory.Name -eq "__pycache__") {
$skip = $true
break
}
}
if (-not $skip) {
$destDir = Split-Path $destPath -Parent
if (-not (Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
Copy-Item -Path $_.FullName -Destination $destPath -Force
}
}
Write-Success "Backend files updated"
# Check if requirements.txt changed
$sourceReq = Join-Path $sourceBackend "requirements.txt"
$destReq = Join-Path $Config.BackendPath "requirements.txt"
if (Test-Path $sourceReq) {
$sourceHash = (Get-FileHash $sourceReq).Hash
$destHash = if (Test-Path $destReq) { (Get-FileHash $destReq).Hash } else { "" }
if ($sourceHash -ne $destHash) {
Write-Step "Requirements changed, updating Python dependencies..."
Copy-Item -Path $sourceReq -Destination $destReq -Force
try {
& python -m pip install -r $destReq --upgrade
Write-Success "Python dependencies updated"
} catch {
Write-Error "Failed to update Python dependencies: $_"
}
} else {
Write-Success "Python dependencies unchanged"
}
}
} catch {
Write-Error "Failed to update backend files: $_"
throw
}
}
function Update-FrontendFiles {
if (-not $UpdateFrontend) {
Write-Warning "Frontend update disabled, skipping..."
return
}
Write-Step "Updating frontend files..."
$sourceFrontend = Join-Path $Config.SourcePath "frontend"
if (-not (Test-Path $sourceFrontend)) {
Write-Warning "Source frontend path not found: $sourceFrontend"
Write-Warning "Expected path: $sourceFrontend"
return
}
try {
# Remove old frontend files (except web.config)
if (Test-Path $Config.FrontendPath) {
Get-ChildItem -Path $Config.FrontendPath -Exclude "web.config" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}
# Copy new frontend files
Copy-Item -Path "$sourceFrontend\*" -Destination $Config.FrontendPath -Recurse -Force
# Ensure web.config exists
$webConfigPath = Join-Path $Config.FrontendPath "web.config"
$webConfigTemplate = Join-Path (Split-Path $PSScriptRoot -Parent) "config\web.config"
if (-not (Test-Path $webConfigPath) -and (Test-Path $webConfigTemplate)) {
Copy-Item -Path $webConfigTemplate -Destination $webConfigPath -Force
Write-Success "web.config restored from template"
}
Write-Success "Frontend files updated"
} catch {
Write-Error "Failed to update frontend files: $_"
throw
}
}
function Start-BackendService {
if (-not $RestartService) {
Write-Warning "Service restart disabled, skipping..."
return
}
Write-Step "Starting backend service..."
try {
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Error "Service $($Config.ServiceName) not found"
return
}
Start-Service -Name $Config.ServiceName
Start-Sleep -Seconds 3
# Wait for service to start
$timeout = 30
$elapsed = 0
while ($service.Status -ne "Running" -and $elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
}
if ($service.Status -eq "Running") {
Write-Success "Service started successfully"
} else {
Write-Error "Service failed to start (Status: $($service.Status))"
Write-Warning "Check logs at: $($Config.LogsPath)\backend-stderr.log"
}
} catch {
Write-Error "Failed to start service: $_"
throw
}
}
function Test-DeploymentHealth {
Write-Step "Testing deployment health..."
Start-Sleep -Seconds 5
try {
# Get service port from .env or use default
$envFile = Join-Path $Config.BackendPath ".env"
$port = 8000
if (Test-Path $envFile) {
$portLine = Get-Content $envFile | Where-Object { $_ -match "^PORT=(\d+)" }
if ($portLine) {
$port = [int]$matches[1]
}
}
# Test backend health endpoint
$healthUrl = "http://localhost:$port/health"
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
Write-Success "Backend health check passed"
Write-Success "Response: $($response.Content)"
} else {
Write-Warning "Health check returned status: $($response.StatusCode)"
}
# Test frontend (IIS)
try {
$frontendResponse = Invoke-WebRequest -Uri "http://localhost/" -UseBasicParsing -TimeoutSec 5
if ($frontendResponse.StatusCode -eq 200) {
Write-Success "Frontend health check passed"
}
} catch {
Write-Warning "Frontend health check failed: $_"
}
} catch {
Write-Warning "Health check failed: $_"
Write-Warning "The service may need more time to start"
Write-Warning "Check logs: $($Config.LogsPath)\backend-stderr.log"
}
}
function Show-DeploymentSummary {
param([string]$BackupPath, [datetime]$StartTime)
$duration = (Get-Date) - $StartTime
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host " DEPLOYMENT COMPLETED" -ForegroundColor Green
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nDeployment Summary:" -ForegroundColor Yellow
Write-Host " Duration: $($duration.TotalSeconds) seconds"
Write-Host " Backend Updated: $UpdateBackend"
Write-Host " Frontend Updated: $UpdateFrontend"
if ($BackupPath) {
Write-Host " Backup Location: $BackupPath"
}
Write-Host "`nApplication Status:" -ForegroundColor Yellow
try {
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
if ($service) {
Write-Host " Backend Service: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" })
}
} catch {
Write-Host " Backend Service: Unknown" -ForegroundColor Yellow
}
Write-Host "`nAccess Points:" -ForegroundColor Yellow
Write-Host " Web Application: http://localhost"
Write-Host " API Documentation: http://localhost/api/docs"
Write-Host "`nManagement:" -ForegroundColor Yellow
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50 -Wait"
Write-Host " Restart Service: .\Restart-ROA2WEB.ps1"
Write-Host " Rollback: Use backup at $BackupPath"
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
# =============================================================================
# MAIN DEPLOYMENT FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB - Deployment Script
Fast deployment and updates for Windows Server + IIS
====================================================================
"@ -ForegroundColor Cyan
$startTime = Get-Date
# Check prerequisites
Write-Step "Checking prerequisites..."
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
exit 1
}
Write-Success "Running as Administrator"
if (-not (Test-Path $Config.InstallPath)) {
Write-Error "Installation path not found: $($Config.InstallPath)"
Write-Host " Run Install-ROA2WEB.ps1 first" -ForegroundColor Yellow
exit 1
}
Write-Success "Installation path exists"
try {
# Deployment steps
$backupPath = Backup-CurrentDeployment
Stop-BackendService
Update-BackendFiles
Update-FrontendFiles
Start-BackendService
Test-DeploymentHealth
Show-DeploymentSummary -BackupPath $backupPath -StartTime $startTime
Write-Host "`nDeployment completed successfully!" -ForegroundColor Green
} catch {
Write-Host "`n[FATAL ERROR] Deployment failed: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
# Attempt rollback if backup exists
if ($backupPath -and (Test-Path $backupPath)) {
Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow
# TODO: Implement rollback logic
}
exit 1
}
}
# Run main deployment
Main

View File

@@ -0,0 +1,598 @@
<#
.SYNOPSIS
ROA2WEB Telegram Bot - Quick Deployment/Update Script for Windows Server
.DESCRIPTION
This script performs rapid deployment or updates of ROA2WEB Telegram Bot:
- Auto-detects source path (use from scripts/ directory)
- Creates backup of current deployment (app files + database)
- Stops bot service
- Updates application files
- Installs new Python dependencies if changed
- Preserves .env configuration
- Restarts bot service
- Validates deployment health
- Rollback support on failure
.PARAMETER InstallPath
Target installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
.PARAMETER SourcePath
Source path for deployment package (auto-detected if run from scripts/)
.PARAMETER BackupEnabled
Create backup before deployment (default: true)
.PARAMETER RestartService
Restart bot service after deployment (default: true)
.PARAMETER RollbackOnFailure
Automatically rollback if deployment fails (default: true)
.EXAMPLE
cd C:\Deploy\TelegramBot\scripts
.\Deploy-TelegramBot.ps1
Deploy from current deployment package (auto-detected)
.EXAMPLE
.\Deploy-TelegramBot.ps1 -SourcePath "C:\Deploy\new-version"
Deploy from specific source path
.EXAMPLE
.\Deploy-TelegramBot.ps1 -BackupEnabled $false -RestartService $false
Update files without backup or restart (manual testing)
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges
Recommended to run from deployment package's scripts/ directory
#>
[CmdletBinding()]
param(
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
[string]$SourcePath = "",
[bool]$BackupEnabled = $true,
[bool]$RestartService = $true,
[bool]$RollbackOnFailure = $true
)
$ErrorActionPreference = "Stop"
# =============================================================================
# CONFIGURATION
# =============================================================================
# Auto-detect source path: if running from scripts/ subdirectory, use parent
$detectedSourcePath = if ($SourcePath) {
$SourcePath
} elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
Split-Path $PSScriptRoot -Parent
} else {
$PSScriptRoot
}
$script:Config = @{
AppName = "ROA2WEB-TelegramBot"
ServiceName = "ROA2WEB-TelegramBot"
InstallPath = $InstallPath
DataPath = Join-Path $InstallPath "data"
BackupPath = Join-Path $InstallPath "backups"
LogsPath = Join-Path $InstallPath "logs"
SourcePath = $detectedSourcePath
}
$script:DeploymentState = @{
BackupPath = $null
ServiceWasRunning = $false
DeploymentSuccess = $false
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function New-BackupDirectory {
if (-not (Test-Path $Config.BackupPath)) {
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
}
}
function Backup-CurrentDeployment {
if (-not $BackupEnabled) {
Write-Warning "Backup disabled, skipping..."
return $null
}
Write-Step "Creating backup of current deployment..."
New-BackupDirectory
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupName = "backup-$timestamp"
$backupFullPath = Join-Path $Config.BackupPath $backupName
try {
# Create backup directory
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
# Backup app directory
if (Test-Path (Join-Path $Config.InstallPath "app")) {
$backupAppPath = Join-Path $backupFullPath "app"
Copy-Item -Path (Join-Path $Config.InstallPath "app") -Destination $backupAppPath -Recurse -Force
Write-Success "App files backed up"
}
# Backup requirements.txt
$reqFile = Join-Path $Config.InstallPath "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 file
$envFile = Join-Path $Config.InstallPath ".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.DataPath "telegram_bot.db"
if (Test-Path $dbFile) {
$backupDataPath = Join-Path $backupFullPath "data"
New-Item -ItemType Directory -Path $backupDataPath -Force | Out-Null
Copy-Item -Path $dbFile -Destination (Join-Path $backupDataPath "telegram_bot.db") -Force
Write-Success "Database backed up"
}
Write-Success "Backup created at: $backupFullPath"
# Clean old backups (keep last 10)
$allBackups = Get-ChildItem -Path $Config.BackupPath -Directory |
Where-Object { $_.Name -like "backup-*" } |
Sort-Object Name -Descending
if ($allBackups.Count -gt 10) {
$oldBackups = $allBackups | Select-Object -Skip 10
foreach ($oldBackup in $oldBackups) {
Remove-Item -Path $oldBackup.FullName -Recurse -Force
Write-Success "Cleaned old backup: $($oldBackup.Name)"
}
}
return $backupFullPath
} catch {
Write-Error "Backup failed: $_"
throw
}
}
function Stop-BotService {
Write-Step "Stopping Telegram bot service..."
try {
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Warning "Service $($Config.ServiceName) not found"
$DeploymentState.ServiceWasRunning = $false
return
}
if ($service.Status -eq "Running") {
$DeploymentState.ServiceWasRunning = $true
Stop-Service -Name $Config.ServiceName -Force
Start-Sleep -Seconds 2
# Wait for service to stop
$timeout = 30
$elapsed = 0
while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
}
if ($service.Status -eq "Stopped") {
Write-Success "Service stopped successfully"
} else {
Write-Warning "Service did not stop within timeout"
}
} else {
Write-Success "Service already stopped"
$DeploymentState.ServiceWasRunning = $false
}
} catch {
Write-Error "Failed to stop service: $_"
throw
}
}
function Update-ApplicationFiles {
Write-Step "Updating application files..."
$sourceApp = Join-Path $Config.SourcePath "app"
if (-not (Test-Path $sourceApp)) {
throw "Source app directory not found: $sourceApp"
}
try {
# Remove old app directory
$destApp = Join-Path $Config.InstallPath "app"
if (Test-Path $destApp) {
Remove-Item -Path $destApp -Recurse -Force
Write-Success "Removed old app directory"
}
# Copy new app files
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
Write-Success "Application files updated"
# Update requirements.txt if present
$sourceReq = Join-Path $Config.SourcePath "requirements.txt"
$destReq = Join-Path $Config.InstallPath "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.InstallPath "venv"
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
if (Test-Path $pipPath) {
try {
& $pipPath install -r $destReq --upgrade
Write-Success "Python dependencies updated"
} catch {
Write-Error "Failed to update Python dependencies: $_"
throw
}
} else {
Write-Warning "Virtual environment not found, skipping dependency update"
}
} else {
Write-Success "Python dependencies unchanged"
}
}
# Preserve .env file (never overwrite)
$envFile = Join-Path $Config.InstallPath ".env"
if (-not (Test-Path $envFile)) {
$sourceEnv = Join-Path $Config.SourcePath ".env.example"
if (Test-Path $sourceEnv) {
Copy-Item -Path $sourceEnv -Destination $envFile -Force
Write-Warning "Created .env from .env.example - PLEASE CONFIGURE"
}
} else {
Write-Success ".env file preserved (not overwritten)"
}
# Update management scripts
$sourceScripts = Join-Path $Config.SourcePath "scripts"
if (Test-Path $sourceScripts) {
$destScripts = Join-Path $Config.InstallPath "scripts"
if (-not (Test-Path $destScripts)) {
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
}
# List of management scripts to update
$managementScripts = @(
"Start-TelegramBot.ps1",
"Stop-TelegramBot.ps1",
"Restart-TelegramBot.ps1",
"Backup-TelegramDB.ps1",
"Setup-DailyBackup.ps1",
"Setup-ClaudeAuth.ps1"
)
$updatedScriptsCount = 0
foreach ($script in $managementScripts) {
$sourcePath = Join-Path $sourceScripts $script
if (Test-Path $sourcePath) {
$destPath = Join-Path $destScripts $script
Copy-Item -Path $sourcePath -Destination $destPath -Force
$updatedScriptsCount++
}
}
if ($updatedScriptsCount -gt 0) {
Write-Success "Updated $updatedScriptsCount management scripts"
}
}
} catch {
Write-Error "Failed to update application files: $_"
throw
}
}
function Start-BotService {
if (-not $RestartService) {
Write-Warning "Service restart disabled, skipping..."
return
}
Write-Step "Starting Telegram bot service..."
try {
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
if (-not $service) {
Write-Warning "Service $($Config.ServiceName) not found"
return
}
Start-Service -Name $Config.ServiceName
Start-Sleep -Seconds 3
# Wait for service to start
$timeout = 30
$elapsed = 0
while ($service.Status -ne "Running" -and $elapsed -lt $timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
}
if ($service.Status -eq "Running") {
Write-Success "Service started successfully"
} else {
throw "Service failed to start (Status: $($service.Status))"
}
} catch {
Write-Error "Failed to start service: $_"
throw
}
}
function Test-DeploymentHealth {
Write-Step "Testing deployment health..."
Start-Sleep -Seconds 5
try {
# Get service port from .env or use default
$envFile = Join-Path $Config.InstallPath ".env"
$port = 8002
if (Test-Path $envFile) {
$envContent = Get-Content $envFile
$portLine = $envContent | Where-Object { $_ -match "^INTERNAL_API_PORT=(\d+)" }
if ($portLine -and $matches[1]) {
$port = [int]$matches[1]
}
}
$healthUrl = "http://localhost:$port/internal/health"
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
$content = $response.Content | ConvertFrom-Json
Write-Success "Health check passed: $($content.status)"
Write-Success "Database: $($content.database.status)"
return $true
} else {
throw "Health check returned status code: $($response.StatusCode)"
}
} catch {
Write-Error "Health check failed: $_"
return $false
}
}
function Restore-FromBackup {
param([string]$BackupPath)
if (-not $BackupPath -or -not (Test-Path $BackupPath)) {
Write-Error "Cannot rollback: backup path not found ($BackupPath)"
return $false
}
Write-Step "Rolling back to backup: $BackupPath"
try {
# Stop service
Stop-BotService
# Restore app directory
$backupApp = Join-Path $BackupPath "app"
$destApp = Join-Path $Config.InstallPath "app"
if (Test-Path $backupApp) {
if (Test-Path $destApp) {
Remove-Item -Path $destApp -Recurse -Force
}
Copy-Item -Path $backupApp -Destination $destApp -Recurse -Force
Write-Success "App files restored"
}
# Restore requirements.txt
$backupReq = Join-Path $BackupPath "requirements.txt"
if (Test-Path $backupReq) {
Copy-Item -Path $backupReq -Destination (Join-Path $Config.InstallPath "requirements.txt") -Force
Write-Success "Requirements file restored"
}
# Restore database
$backupDb = Join-Path $BackupPath "data\telegram_bot.db"
if (Test-Path $backupDb) {
Copy-Item -Path $backupDb -Destination (Join-Path $Config.DataPath "telegram_bot.db") -Force
Write-Success "Database restored"
}
# Restart service
if ($DeploymentState.ServiceWasRunning) {
Start-BotService
}
Write-Success "Rollback completed successfully"
return $true
} catch {
Write-Error "Rollback failed: $_"
return $false
}
}
function Show-DeploymentSummary {
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
if ($DeploymentState.DeploymentSuccess) {
Write-Host " DEPLOYMENT COMPLETED SUCCESSFULLY" -ForegroundColor Green
} else {
Write-Host " DEPLOYMENT FAILED" -ForegroundColor Red
}
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nDeployment Details:" -ForegroundColor Yellow
Write-Host " Install Path: $($Config.InstallPath)"
Write-Host " Source Path: $($Config.SourcePath)"
Write-Host " Backup Created: $(if ($DeploymentState.BackupPath) { 'Yes' } else { 'No' })"
if ($DeploymentState.BackupPath) {
Write-Host " Backup Location: $($DeploymentState.BackupPath)"
}
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
if ($service) {
Write-Host " Service Status: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" })
}
if ($DeploymentState.DeploymentSuccess) {
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host " - Monitor logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50 -Wait"
Write-Host " - Check health: Invoke-WebRequest http://localhost:8002/internal/health"
Write-Host " - Test bot on Telegram"
} else {
Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
Write-Host " - Check logs: Get-Content $($Config.LogsPath)\stderr.log -Tail 100"
Write-Host " - Verify .env configuration"
if ($DeploymentState.BackupPath -and $RollbackOnFailure) {
Write-Host " - Rollback completed automatically to: $($DeploymentState.BackupPath)"
} elseif ($DeploymentState.BackupPath) {
Write-Host " - Manual rollback available at: $($DeploymentState.BackupPath)"
}
}
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
# =============================================================================
# MAIN DEPLOYMENT FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB Telegram Bot - Deployment Script
Quick deployment and update automation
====================================================================
"@ -ForegroundColor Cyan
# Check prerequisites
Write-Step "Checking prerequisites..."
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
exit 1
}
Write-Success "Running as Administrator"
if (-not (Test-Path $Config.InstallPath)) {
Write-Error "Installation path not found: $($Config.InstallPath)"
Write-Host " Run Install-TelegramBot.ps1 first" -ForegroundColor Yellow
exit 1
}
Write-Success "Installation path verified"
if (-not (Test-Path $Config.SourcePath)) {
Write-Error "Source path not found: $($Config.SourcePath)"
exit 1
}
Write-Success "Source path verified: $($Config.SourcePath)"
try {
# Deployment steps
$DeploymentState.BackupPath = Backup-CurrentDeployment
Stop-BotService
Update-ApplicationFiles
Start-BotService
$healthOk = Test-DeploymentHealth
if ($healthOk) {
$DeploymentState.DeploymentSuccess = $true
Write-Host "`nDeployment completed successfully!" -ForegroundColor Green
} else {
throw "Health check failed after deployment"
}
} catch {
Write-Host "`n[DEPLOYMENT FAILED] $_" -ForegroundColor Red
if ($RollbackOnFailure -and $DeploymentState.BackupPath) {
Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow
$rollbackOk = Restore-FromBackup -BackupPath $DeploymentState.BackupPath
if ($rollbackOk) {
Write-Host "Rollback completed successfully" -ForegroundColor Yellow
} else {
Write-Host "Rollback failed - manual intervention required" -ForegroundColor Red
}
} else {
Write-Host "Automatic rollback disabled or no backup available" -ForegroundColor Yellow
}
$DeploymentState.DeploymentSuccess = $false
} finally {
Show-DeploymentSummary
}
if (-not $DeploymentState.DeploymentSuccess) {
exit 1
}
}
# Run main deployment
Main

View File

@@ -0,0 +1,382 @@
<#
.SYNOPSIS
Enable HTTPS for ROA2WEB on IIS
.DESCRIPTION
This script configures HTTPS for ROA2WEB by:
- Creating a self-signed SSL certificate (or using existing certificate)
- Configuring HTTPS binding on IIS site
- Enabling HTTP to HTTPS redirect
.PARAMETER IISSiteName
IIS Site name (default: Default Web Site)
.PARAMETER CertificateDnsName
DNS name for certificate (default: server IP or hostname)
.PARAMETER UseExistingCert
Use existing certificate by thumbprint
.PARAMETER CertThumbprint
Thumbprint of existing certificate to use
.PARAMETER Port
HTTPS port (default: 443)
.EXAMPLE
.\Enable-HTTPS.ps1
Create self-signed certificate and enable HTTPS
.EXAMPLE
.\Enable-HTTPS.ps1 -IISSiteName "ROA2WEB" -CertificateDnsName "roa2web.company.com"
Create certificate with custom DNS name
.EXAMPLE
.\Enable-HTTPS.ps1 -UseExistingCert -CertThumbprint "ABC123..."
Use existing certificate
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges
#>
[CmdletBinding()]
param(
[string]$IISSiteName = "Default Web Site",
[string]$CertificateDnsName = "",
[switch]$UseExistingCert,
[string]$CertThumbprint = "",
[int]$Port = 443,
[string]$IPAddress = "*"
)
$ErrorActionPreference = "Stop"
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
# =============================================================================
# MAIN SCRIPT
# =============================================================================
Write-Host @"
====================================================================
ROA2WEB - Enable HTTPS on IIS
====================================================================
"@ -ForegroundColor Cyan
# Check administrator privileges
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
exit 1
}
Write-Success "Running as Administrator"
# Import required modules
Write-Step "Loading IIS module..."
Import-Module WebAdministration -ErrorAction Stop
Write-Success "IIS module loaded"
# Verify IIS site exists
Write-Step "Verifying IIS site '$IISSiteName'..."
$site = Get-Website -Name $IISSiteName -ErrorAction SilentlyContinue
if (-not $site) {
Write-Error "IIS site '$IISSiteName' not found"
Write-Host "`nAvailable sites:" -ForegroundColor Yellow
Get-Website | Format-Table Name, State, PhysicalPath
exit 1
}
Write-Success "IIS site found: $IISSiteName (State: $($site.State))"
# Get or create certificate
if ($UseExistingCert) {
Write-Step "Using existing certificate..."
if ([string]::IsNullOrEmpty($CertThumbprint)) {
Write-Host "`nAvailable certificates in LocalMachine\My:" -ForegroundColor Yellow
Get-ChildItem -Path "cert:\LocalMachine\My" |
Select-Object Thumbprint, Subject, FriendlyName, NotAfter |
Format-Table -AutoSize
$CertThumbprint = Read-Host "Enter certificate thumbprint"
}
$cert = Get-ChildItem -Path "cert:\LocalMachine\My\$CertThumbprint" -ErrorAction SilentlyContinue
if (-not $cert) {
Write-Error "Certificate with thumbprint '$CertThumbprint' not found"
exit 1
}
Write-Success "Certificate found: $($cert.Subject)"
} else {
Write-Step "Creating self-signed SSL certificate..."
# Auto-detect DNS name if not provided
if ([string]::IsNullOrEmpty($CertificateDnsName)) {
$serverName = $env:COMPUTERNAME
$ipAddress = (Get-NetIPAddress -AddressFamily IPv4 |
Where-Object {$_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.*"} |
Select-Object -First 1).IPAddress
Write-Host " Detected hostname: $serverName" -ForegroundColor Yellow
Write-Host " Detected IP: $ipAddress" -ForegroundColor Yellow
$response = Read-Host "Use hostname for certificate? (Y/n)"
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
$CertificateDnsName = $serverName
} else {
$CertificateDnsName = $ipAddress
}
}
# Check if certificate already exists
$existingCert = Get-ChildItem -Path "cert:\LocalMachine\My" |
Where-Object {$_.DnsNameList -contains $CertificateDnsName} |
Select-Object -First 1
if ($existingCert) {
Write-Warning "Certificate for '$CertificateDnsName' already exists"
$response = Read-Host "Use existing certificate? (Y/n)"
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
$cert = $existingCert
Write-Success "Using existing certificate"
} else {
Write-Warning "Removing existing certificate..."
Remove-Item -Path "cert:\LocalMachine\My\$($existingCert.Thumbprint)" -Force
}
}
if (-not $cert) {
# Create new certificate
$cert = New-SelfSignedCertificate `
-DnsName $CertificateDnsName `
-CertStoreLocation "cert:\LocalMachine\My" `
-NotAfter (Get-Date).AddYears(5) `
-FriendlyName "ROA2WEB SSL Certificate" `
-KeyUsage DigitalSignature, KeyEncipherment `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")
Write-Success "Certificate created successfully"
Write-Host " DNS Name: $CertificateDnsName" -ForegroundColor Yellow
Write-Host " Thumbprint: $($cert.Thumbprint)" -ForegroundColor Yellow
Write-Host " Valid Until: $($cert.NotAfter)" -ForegroundColor Yellow
}
}
# Configure HTTPS binding
Write-Step "Configuring HTTPS binding..."
# Check if HTTPS binding already exists
$existingBinding = Get-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -ErrorAction SilentlyContinue
if ($existingBinding) {
Write-Warning "HTTPS binding already exists on port $Port"
$response = Read-Host "Remove and recreate binding? (Y/n)"
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
Remove-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -ErrorAction SilentlyContinue
Write-Success "Existing binding removed"
} else {
Write-Warning "Skipping binding creation"
$existingBinding = $null
}
}
if (-not $existingBinding) {
# Create HTTPS binding
try {
New-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -IPAddress $IPAddress
Write-Success "HTTPS binding created on port $Port"
} catch {
Write-Error "Failed to create HTTPS binding: $_"
exit 1
}
}
# Attach certificate to binding
Write-Step "Attaching certificate to HTTPS binding..."
try {
# Method 1: Using IIS PowerShell Provider (IIS 8+)
Push-Location
Set-Location IIS:\SslBindings
# Remove existing binding if present
$sslBinding = "${IPAddress}!${Port}"
if ($IPAddress -eq "*") {
$sslBinding = "0.0.0.0!${Port}"
}
if (Test-Path $sslBinding) {
Remove-Item $sslBinding -Force -ErrorAction SilentlyContinue
}
# Create new SSL binding
$cert | New-Item $sslBinding
Pop-Location
Write-Success "Certificate attached to HTTPS binding"
} catch {
Write-Warning "Method 1 failed, trying alternative method..."
try {
# Method 2: Using netsh (more compatible)
$certHash = $cert.Thumbprint
$appId = "{4dc3e181-e14b-4a21-b022-59fc669b0914}"
# Remove existing binding
netsh http delete sslcert ipport=0.0.0.0:$Port 2>&1 | Out-Null
# Add new binding
$result = netsh http add sslcert ipport=0.0.0.0:$Port certhash=$certHash appid=$appId
if ($LASTEXITCODE -eq 0) {
Write-Success "Certificate attached using netsh"
} else {
Write-Error "Failed to attach certificate: $result"
exit 1
}
} catch {
Write-Error "Failed to attach certificate: $_"
exit 1
}
}
# Update web.config for HTTP to HTTPS redirect
Write-Step "Checking web.config for HTTPS redirect..."
$webConfigPath = Join-Path $site.PhysicalPath "web.config"
if (Test-Path $webConfigPath) {
$webConfig = Get-Content $webConfigPath -Raw
if ($webConfig -match "Force HTTPS") {
Write-Success "HTTPS redirect rule already exists in web.config"
} else {
Write-Warning "HTTPS redirect rule not found in web.config"
Write-Host "`nTo enable automatic HTTP to HTTPS redirect, add this rule to web.config:" -ForegroundColor Yellow
Write-Host @"
<rule name="Force HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
"@ -ForegroundColor Gray
$response = Read-Host "`nAdd this rule automatically? (Y/n)"
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
# Backup web.config
$backupPath = "$webConfigPath.backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Copy-Item -Path $webConfigPath -Destination $backupPath
Write-Success "web.config backed up to: $backupPath"
# Add redirect rule
$redirectRule = @"
<!-- Rule: Force HTTPS (redirect HTTP to HTTPS) -->
<rule name="Force HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
"@
$webConfig = $webConfig -replace '(<rules>)', "`$1`r`n$redirectRule"
Set-Content -Path $webConfigPath -Value $webConfig
Write-Success "HTTPS redirect rule added to web.config"
}
}
} else {
Write-Warning "web.config not found at: $webConfigPath"
Write-Host " Please ensure web.config exists and contains proper rewrite rules" -ForegroundColor Yellow
}
# Test configuration
Write-Step "Testing configuration..."
# Restart IIS site to apply changes
try {
Stop-Website -Name $IISSiteName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Start-Website -Name $IISSiteName
Write-Success "IIS site restarted"
} catch {
Write-Warning "Could not restart IIS site: $_"
}
# Display summary
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host " HTTPS CONFIGURATION COMPLETED" -ForegroundColor Green
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host "`nConfiguration Summary:" -ForegroundColor Yellow
Write-Host " IIS Site: $IISSiteName"
Write-Host " HTTPS Port: $Port"
Write-Host " Certificate: $($cert.Subject)"
Write-Host " Thumbprint: $($cert.Thumbprint)"
Write-Host " Valid Until: $($cert.NotAfter)"
Write-Host "`nAccess Points:" -ForegroundColor Yellow
Write-Host " HTTPS URL: https://$CertificateDnsName"
if ($site.Bindings.Collection | Where-Object {$_.protocol -eq "http"}) {
Write-Host " HTTP URL: http://$CertificateDnsName (will redirect to HTTPS)"
}
Write-Host "`nNext Steps:" -ForegroundColor Yellow
if ($cert.Subject -match "CN=[\d\.]+") {
Write-Host " 1. [RECOMMENDED] Replace self-signed certificate with CA-issued certificate"
Write-Host " - Browsers will show security warnings for self-signed certificates"
Write-Host " - Use Let's Encrypt, DigiCert, or another CA for production"
}
Write-Host " 2. Test HTTPS access: https://$CertificateDnsName"
Write-Host " 3. Verify HTTP to HTTPS redirect is working"
Write-Host " 4. Check browser console for mixed content warnings"
Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
Write-Host " View IIS bindings: Get-WebBinding -Name '$IISSiteName'"
Write-Host " View certificates: Get-ChildItem cert:\LocalMachine\My"
Write-Host " Test HTTPS locally: Invoke-WebRequest https://localhost:$Port -SkipCertificateCheck"
Write-Host " IIS Manager: inetmgr"
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host ""

View File

@@ -0,0 +1,598 @@
<#
.SYNOPSIS
ROA2WEB - Initial Installation Script for Windows Server + IIS
.DESCRIPTION
This script performs complete installation of ROA2WEB on Windows Server:
- Checks prerequisites (Admin rights, IIS)
- Installs Python 3.11+ if needed
- Installs NSSM (service manager)
- Installs IIS URL Rewrite and ARR modules
- Creates directory structure
- Installs Python dependencies
- Creates Windows Service for backend
- Configures IIS website
- Starts all services
.PARAMETER InstallPath
Installation path (default: C:\inetpub\wwwroot\roa2web)
.PARAMETER PythonVersion
Python version to install (default: 3.11.9)
.PARAMETER ServicePort
Backend service port (default: 8000)
.PARAMETER SkipPython
Skip Python installation (use existing Python)
.PARAMETER SkipIIS
Skip IIS configuration
.EXAMPLE
.\Install-ROA2WEB.ps1
Standard installation with defaults
.EXAMPLE
.\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web" -ServicePort 8001
Custom installation path and port
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges
#>
[CmdletBinding()]
param(
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
[string]$PythonVersion = "3.11.9",
[int]$ServicePort = 8000,
[string]$IISSiteName = "Default Web Site",
[string]$IISAppName = "roa2web",
[switch]$CreateNewSite,
[switch]$SkipPython,
[switch]$SkipIIS
)
# Strict error handling
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# =============================================================================
# CONFIGURATION
# =============================================================================
$script:Config = @{
AppName = "ROA2WEB"
ServiceName = "ROA2WEB-Backend"
ServiceDisplayName = "ROA2WEB Backend Service"
ServiceDescription = "FastAPI backend service for ROA2WEB ERP Reports Application"
InstallPath = $InstallPath
BackendPath = Join-Path $InstallPath "backend"
FrontendPath = Join-Path $InstallPath "frontend"
LogsPath = Join-Path $InstallPath "logs"
TempPath = Join-Path $InstallPath "temp"
PythonVersion = $PythonVersion
ServicePort = $ServicePort
IISSiteName = $IISSiteName
IISAppName = $IISAppName
IISAppPoolName = "ROA2WEB-AppPool"
CreateNewSite = $CreateNewSite
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-CommandExists {
param([string]$Command)
try {
if (Get-Command $Command -ErrorAction Stop) {
return $true
}
} catch {
return $false
}
}
function Install-Chocolatey {
Write-Step "Installing Chocolatey package manager..."
if (Test-CommandExists "choco") {
Write-Success "Chocolatey already installed"
return
}
try {
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Refresh environment
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Success "Chocolatey installed successfully"
} catch {
throw "Failed to install Chocolatey: $_"
}
}
function Install-Python {
Write-Step "Checking Python installation..."
if ($SkipPython) {
Write-Warning "Skipping Python installation (as requested)"
return
}
# Check if Python is already installed
try {
$pythonCmd = Get-Command python -ErrorAction Stop
$pythonVersionOutput = & python --version 2>&1
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
$installedVersion = $matches[1]
Write-Success "Python $installedVersion already installed at $($pythonCmd.Source)"
return
}
} catch {
Write-Warning "Python not found, will install..."
}
# Install Python via Chocolatey
Write-Step "Installing Python $PythonVersion..."
try {
choco install python --version=$PythonVersion -y --force
# Refresh environment
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Success "Python $PythonVersion installed successfully"
} catch {
throw "Failed to install Python: $_"
}
}
function Install-NSSM {
Write-Step "Installing NSSM (service manager)..."
if (Test-Path "C:\nssm\nssm.exe") {
Write-Success "NSSM already installed"
return
}
try {
choco install nssm -y
Write-Success "NSSM installed successfully"
} catch {
throw "Failed to install NSSM: $_"
}
}
function Install-IISModules {
if ($SkipIIS) {
Write-Warning "Skipping IIS configuration (as requested)"
return
}
Write-Step "Checking IIS installation..."
# Detect OS type (Server vs Desktop)
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
$isServer = $osInfo.ProductType -eq 3 # 1=Workstation, 2=Domain Controller, 3=Server
# Check if IIS is installed (different cmdlets for Server vs Desktop)
$iisInstalled = $false
if ($isServer) {
# Windows Server - use Get-WindowsFeature
$iisFeature = Get-WindowsFeature -Name Web-Server -ErrorAction SilentlyContinue
$iisInstalled = $iisFeature -and $iisFeature.InstallState -eq "Installed"
if (-not $iisInstalled) {
Write-Error "IIS is not installed. Please install IIS first:"
Write-Host " Install-WindowsFeature -Name Web-Server -IncludeManagementTools" -ForegroundColor Yellow
throw "IIS not installed"
}
} else {
# Windows Desktop (10/11) - use Get-WindowsOptionalFeature
$iisFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -ErrorAction SilentlyContinue
$iisInstalled = $iisFeature -and $iisFeature.State -eq "Enabled"
if (-not $iisInstalled) {
Write-Error "IIS is not installed. Please install IIS first:"
Write-Host " Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -All" -ForegroundColor Yellow
Write-Host " Or use: Control Panel -> Programs -> Turn Windows features on/off -> Internet Information Services" -ForegroundColor Yellow
throw "IIS not installed"
}
}
Write-Success "IIS is installed ($($osInfo.Caption))"
# Install URL Rewrite Module
Write-Step "Installing IIS URL Rewrite Module..."
$urlRewriteInstalled = Get-WebConfiguration -Filter "/system.webServer/rewrite" -PSPath "IIS:\" -ErrorAction SilentlyContinue
if (-not $urlRewriteInstalled) {
Write-Warning "URL Rewrite not found, installing..."
try {
$urlRewriteUrl = "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi"
$urlRewritePath = "$env:TEMP\rewrite_amd64.msi"
Invoke-WebRequest -Uri $urlRewriteUrl -OutFile $urlRewritePath
Start-Process msiexec.exe -ArgumentList "/i", $urlRewritePath, "/quiet", "/norestart" -Wait
Remove-Item $urlRewritePath -Force
Write-Success "URL Rewrite Module installed"
} catch {
Write-Error "Failed to install URL Rewrite: $_"
Write-Warning "You can download it manually from: https://www.iis.net/downloads/microsoft/url-rewrite"
}
} else {
Write-Success "URL Rewrite Module already installed"
}
# Install Application Request Routing (ARR)
Write-Step "Checking Application Request Routing (ARR)..."
try {
choco install iis-arr -y
Write-Success "ARR installed successfully"
} catch {
Write-Warning "Could not install ARR via Chocolatey. Download manually from: https://www.iis.net/downloads/microsoft/application-request-routing"
}
}
function New-DirectoryStructure {
Write-Step "Creating directory structure..."
$directories = @(
$Config.InstallPath,
$Config.BackendPath,
$Config.FrontendPath,
$Config.LogsPath,
$Config.TempPath,
(Join-Path $Config.BackendPath "logs"),
(Join-Path $Config.BackendPath "temp")
)
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 (IIS user needs read access)
try {
$acl = Get-Acl $Config.InstallPath
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($accessRule)
Set-Acl -Path $Config.InstallPath -AclObject $acl
Write-Success "Permissions set for IIS_IUSRS"
} catch {
Write-Warning "Could not set permissions: $_"
}
}
function Install-PythonDependencies {
Write-Step "Installing Python dependencies..."
$requirementsPath = Join-Path $Config.BackendPath "requirements.txt"
if (-not (Test-Path $requirementsPath)) {
Write-Warning "requirements.txt not found at $requirementsPath"
Write-Warning "Please copy backend files first, then run this script again"
return
}
try {
# Upgrade pip first
& python -m pip install --upgrade pip
# Install dependencies
& python -m pip install -r $requirementsPath
Write-Success "Python dependencies installed successfully"
} catch {
throw "Failed to install Python dependencies: $_"
}
}
function New-WindowsService {
Write-Step "Creating Windows Service for backend..."
# Check if service already exists using nssm (more reliable than Get-Service)
# Temporarily disable error action to check service status
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$nssmOutput = & nssm status $Config.ServiceName 2>&1
$serviceExists = $LASTEXITCODE -eq 0
$ErrorActionPreference = $oldErrorAction
if ($serviceExists) {
Write-Warning "Service already exists, stopping and removing..."
# Try to stop service (may fail if service is broken)
& nssm stop $Config.ServiceName 2>&1 | Out-Null
Start-Sleep -Seconds 2
# Force remove service
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
Start-Sleep -Seconds 2
Write-Success "Existing service removed"
}
# Get Python path
$pythonPath = (Get-Command python).Source
$uvicornModule = "uvicorn"
$appModule = "app.main:app"
# NSSM service creation
try {
# Install service
& nssm install $Config.ServiceName $pythonPath "-m" $uvicornModule $appModule "--host" "127.0.0.1" "--port" $Config.ServicePort.ToString() "--workers" "4"
# Set service configuration
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
& nssm set $Config.ServiceName Description $Config.ServiceDescription
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
& nssm set $Config.ServiceName AppDirectory $Config.BackendPath
# Set environment variables (PYTHONPATH for shared modules)
# Point to the installation root so shared/ modules can be imported
$pythonPath = $Config.InstallPath
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$pythonPath"
# Set logging
$stdoutLog = Join-Path $Config.LogsPath "backend-stdout.log"
$stderrLog = Join-Path $Config.LogsPath "backend-stderr.log"
& nssm set $Config.ServiceName AppStdout $stdoutLog
& nssm set $Config.ServiceName AppStderr $stderrLog
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
# Set restart policy
& nssm set $Config.ServiceName AppExit Default Restart
& nssm set $Config.ServiceName AppRestartDelay 5000
Write-Success "Windows Service created successfully"
} catch {
throw "Failed to create Windows Service: $_"
}
}
function Initialize-IISWebsite {
if ($SkipIIS) {
Write-Warning "Skipping IIS website configuration (as requested)"
return
}
Write-Step "Configuring IIS application..."
Import-Module WebAdministration -ErrorAction Stop
# Remove existing app pool if present
if (Test-Path "IIS:\AppPools\$($Config.IISAppPoolName)") {
Write-Warning "Removing existing app pool..."
Remove-WebAppPool -Name $Config.IISAppPoolName -ErrorAction SilentlyContinue
}
# Create Application Pool
Write-Step "Creating IIS Application Pool..."
New-WebAppPool -Name $Config.IISAppPoolName -Force | Out-Null
Set-ItemProperty -Path "IIS:\AppPools\$($Config.IISAppPoolName)" -Name "managedRuntimeVersion" -Value ""
Write-Success "Application Pool created: $($Config.IISAppPoolName)"
if ($CreateNewSite) {
# Create new website (old behavior)
Write-Step "Creating new IIS Website..."
# Stop default website if running
try {
Stop-Website -Name "Default Web Site" -ErrorAction SilentlyContinue
Write-Success "Stopped Default Web Site"
} catch {
Write-Warning "Could not stop Default Web Site: $_"
}
# Remove existing site if present
if (Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue) {
Write-Warning "Removing existing website..."
Remove-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
}
New-Website -Name $Config.IISSiteName `
-PhysicalPath $Config.FrontendPath `
-ApplicationPool $Config.IISAppPoolName `
-Port 80 `
-Force | Out-Null
Write-Success "Website created: $($Config.IISSiteName)"
# Start website
Start-Website -Name $Config.IISSiteName
Write-Success "Website started: $($Config.IISSiteName)"
} else {
# Create application under existing site (default behavior)
Write-Step "Creating IIS Application under '$($Config.IISSiteName)'..."
# Verify parent site exists
$parentSite = Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
if (-not $parentSite) {
throw "Parent website '$($Config.IISSiteName)' does not exist. Use -CreateNewSite to create a new site."
}
# Remove existing application if present
$existingApp = Get-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
if ($existingApp) {
Write-Warning "Removing existing application..."
Remove-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
}
# Create application
New-WebApplication -Name $Config.IISAppName `
-Site $Config.IISSiteName `
-PhysicalPath $Config.FrontendPath `
-ApplicationPool $Config.IISAppPoolName `
-Force | Out-Null
Write-Success "Application created: /$($Config.IISAppName) under $($Config.IISSiteName)"
}
# Copy web.config to frontend path
$webConfigSource = Join-Path $PSScriptRoot "..\config\web.config"
$webConfigDest = Join-Path $Config.FrontendPath "web.config"
if (Test-Path $webConfigSource) {
Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force
Write-Success "web.config copied to frontend path"
} else {
Write-Warning "web.config not found at $webConfigSource"
}
}
function Start-Services {
Write-Step "Starting services..."
# Start backend service
try {
Start-Service -Name $Config.ServiceName
Start-Sleep -Seconds 3
$service = Get-Service -Name $Config.ServiceName
if ($service.Status -eq "Running") {
Write-Success "Backend service started successfully"
} else {
Write-Error "Backend service failed to start (Status: $($service.Status))"
}
} catch {
Write-Error "Failed to start backend service: $_"
}
# Test backend health
Write-Step "Testing backend health..."
Start-Sleep -Seconds 5
try {
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/health" -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
Write-Success "Backend health check passed"
}
} catch {
Write-Warning "Backend health check failed (may need time to start): $_"
}
}
function Show-Summary {
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host " ROA2WEB INSTALLATION COMPLETED" -ForegroundColor Green
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
Write-Host " Install Path: $($Config.InstallPath)"
Write-Host " Backend Path: $($Config.BackendPath)"
Write-Host " Frontend Path: $($Config.FrontendPath)"
Write-Host " Service Name: $($Config.ServiceName)"
Write-Host " Service Port: $($Config.ServicePort)"
Write-Host " IIS Site: $($Config.IISSiteName)"
Write-Host "`nAccess Points:" -ForegroundColor Yellow
if ($Config.CreateNewSite) {
Write-Host " Web Application: http://localhost"
} else {
Write-Host " Web Application: http://localhost/$($Config.IISAppName)"
}
Write-Host " API Backend: http://localhost:$($Config.ServicePort)"
Write-Host " API Docs: http://localhost:$($Config.ServicePort)/docs"
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/health"
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host " 1. Copy backend files to: $($Config.BackendPath)"
Write-Host " 2. Copy frontend files to: $($Config.FrontendPath)"
Write-Host " 3. Configure .env file at: $($Config.BackendPath)\.env"
Write-Host " 4. Run: .\Deploy-ROA2WEB.ps1 to deploy updates"
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
Write-Host " Start Service: .\Start-ROA2WEB.ps1"
Write-Host " Stop Service: .\Stop-ROA2WEB.ps1"
Write-Host " Restart Service: .\Restart-ROA2WEB.ps1"
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50"
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
# =============================================================================
# MAIN INSTALLATION FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB - Windows Server Installation Script
Modern ERP Reports Application with FastAPI + Vue.js + IIS
====================================================================
"@ -ForegroundColor Cyan
# Check prerequisites
Write-Step "Checking prerequisites..."
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
exit 1
}
Write-Success "Running as Administrator"
try {
# Installation steps
Install-Chocolatey
Install-Python
Install-NSSM
Install-IISModules
New-DirectoryStructure
Install-PythonDependencies
New-WindowsService
Initialize-IISWebsite
Start-Services
Show-Summary
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
} catch {
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
exit 1
}
}
# Run main installation
Main

View File

@@ -0,0 +1,633 @@
<#
.SYNOPSIS
ROA2WEB Telegram Bot - Installation Script for Windows Server
.DESCRIPTION
This script performs complete installation of ROA2WEB Telegram Bot on Windows Server:
- Checks prerequisites (Admin rights, Python)
- Installs NSSM (service manager) if needed
- Creates directory structure
- Installs Python dependencies
- Creates Windows Service for Telegram bot
- Configures internal API
- Starts service
.PARAMETER InstallPath
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
.PARAMETER ServicePort
Internal API service port (default: 8002)
.PARAMETER SourcePath
Source path for deployment package (auto-detected if run from scripts/ directory)
.PARAMETER SkipPython
Skip Python installation check (use existing Python)
.EXAMPLE
.\Install-TelegramBot.ps1
Standard installation with defaults (auto-detects source from scripts/ directory)
.EXAMPLE
.\Install-TelegramBot.ps1 -InstallPath "D:\Apps\roa2web\telegram-bot" -ServicePort 8003
Custom installation path and port
.EXAMPLE
.\Install-TelegramBot.ps1 -SourcePath "C:\Deploy\telegram-bot"
Install from specific source path
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges, Python 3.11+
#>
[CmdletBinding()]
param(
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
[int]$ServicePort = 8002,
[string]$SourcePath = "",
[switch]$SkipPython
)
# Strict error handling
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# =============================================================================
# CONFIGURATION
# =============================================================================
# Auto-detect source path: if running from scripts/ subdirectory, use parent
$detectedSourcePath = if ($SourcePath) {
$SourcePath
} elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
Split-Path $PSScriptRoot -Parent
} else {
$PSScriptRoot
}
$script:Config = @{
AppName = "ROA2WEB-TelegramBot"
ServiceName = "ROA2WEB-TelegramBot"
ServiceDisplayName = "ROA2WEB Telegram Bot Service"
ServiceDescription = "Telegram bot frontend for ROA2WEB with Claude Agent SDK"
InstallPath = $InstallPath
DataPath = Join-Path $InstallPath "data"
LogsPath = Join-Path $InstallPath "logs"
TempPath = Join-Path $InstallPath "temp"
BackupPath = Join-Path $InstallPath "backups"
ServicePort = $ServicePort
SourcePath = $detectedSourcePath
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-CommandExists {
param([string]$Command)
try {
if (Get-Command $Command -ErrorAction Stop) {
return $true
}
} catch {
return $false
}
}
function Test-PythonInstallation {
Write-Step "Checking Python installation..."
if ($SkipPython) {
Write-Warning "Skipping Python check (as requested)"
return
}
try {
$pythonCmd = Get-Command python -ErrorAction Stop
$pythonVersionOutput = & python --version 2>&1
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
$installedVersion = $matches[1]
$versionParts = $installedVersion -split '\.'
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
if ($major -ge 3 -and $minor -ge 11) {
Write-Success "Python $installedVersion found at $($pythonCmd.Source)"
return
} else {
throw "Python 3.11+ required, found $installedVersion"
}
}
} catch {
Write-Error "Python 3.11+ not found. Please install Python first."
Write-Host " Download from: https://www.python.org/downloads/" -ForegroundColor Yellow
throw "Python not installed"
}
}
function Copy-ApplicationFiles {
Write-Step "Copying application files from deployment package..."
$sourceApp = Join-Path $Config.SourcePath "app"
$sourceReq = Join-Path $Config.SourcePath "requirements.txt"
$sourceScripts = Join-Path $Config.SourcePath "scripts"
# Validate source files exist
if (-not (Test-Path $sourceApp)) {
Write-Warning "Source app directory not found: $sourceApp"
Write-Warning "You may need to copy application files manually"
return $false
}
if (-not (Test-Path $sourceReq)) {
Write-Warning "Source requirements.txt not found: $sourceReq"
Write-Warning "You may need to copy requirements.txt manually"
return $false
}
try {
# Copy app directory
$destApp = Join-Path $Config.InstallPath "app"
if (Test-Path $destApp) {
Write-Warning "App directory already exists, removing..."
Remove-Item -Path $destApp -Recurse -Force
}
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
Write-Success "Application files copied"
# Copy requirements.txt
$destReq = Join-Path $Config.InstallPath "requirements.txt"
Copy-Item -Path $sourceReq -Destination $destReq -Force
Write-Success "requirements.txt copied"
# Copy .env.example if exists (but don't overwrite .env)
$sourceEnvExample = Join-Path $Config.SourcePath ".env.example"
if (Test-Path $sourceEnvExample) {
$destEnvExample = Join-Path $Config.InstallPath ".env.example"
Copy-Item -Path $sourceEnvExample -Destination $destEnvExample -Force
Write-Success ".env.example copied"
}
# Copy management scripts (but exclude installation/deployment scripts)
if (Test-Path $sourceScripts) {
$destScripts = Join-Path $Config.InstallPath "scripts"
if (-not (Test-Path $destScripts)) {
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
}
# List of management scripts to copy
$managementScripts = @(
"Start-TelegramBot.ps1",
"Stop-TelegramBot.ps1",
"Restart-TelegramBot.ps1",
"Backup-TelegramDB.ps1",
"Setup-DailyBackup.ps1",
"Setup-ClaudeAuth.ps1"
)
$copiedScriptsCount = 0
foreach ($script in $managementScripts) {
$sourcePath = Join-Path $sourceScripts $script
if (Test-Path $sourcePath) {
$destPath = Join-Path $destScripts $script
Copy-Item -Path $sourcePath -Destination $destPath -Force
$copiedScriptsCount++
}
}
if ($copiedScriptsCount -gt 0) {
Write-Success "Copied $copiedScriptsCount management scripts to scripts/ directory"
} else {
Write-Warning "No management scripts found to copy"
}
} else {
Write-Warning "Source scripts directory not found: $sourceScripts"
}
return $true
} catch {
Write-Error "Failed to copy application files: $_"
return $false
}
}
function Install-NSSM {
Write-Step "Installing NSSM (service manager)..."
if (Test-Path "C:\nssm\nssm.exe") {
Write-Success "NSSM already installed"
return
}
# Check if Chocolatey is available
if (Test-CommandExists "choco") {
try {
choco install nssm -y
Write-Success "NSSM installed via Chocolatey"
return
} catch {
Write-Warning "Chocolatey installation failed, trying direct download..."
}
}
# Direct download as fallback
try {
$nssmUrl = "https://nssm.cc/release/nssm-2.24.zip"
$nssmZip = "$env:TEMP\nssm.zip"
$nssmExtract = "$env:TEMP\nssm"
Write-Step "Downloading NSSM..."
Invoke-WebRequest -Uri $nssmUrl -OutFile $nssmZip
Write-Step "Extracting NSSM..."
Expand-Archive -Path $nssmZip -DestinationPath $nssmExtract -Force
# Copy nssm.exe to C:\nssm
New-Item -ItemType Directory -Path "C:\nssm" -Force | Out-Null
Copy-Item -Path "$nssmExtract\nssm-2.24\win64\nssm.exe" -Destination "C:\nssm\nssm.exe" -Force
# Add to PATH
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
if ($currentPath -notlike "*C:\nssm*") {
[Environment]::SetEnvironmentVariable("Path", "$currentPath;C:\nssm", "Machine")
$env:Path += ";C:\nssm"
}
# Cleanup
Remove-Item $nssmZip -Force
Remove-Item $nssmExtract -Recurse -Force
Write-Success "NSSM installed successfully"
} catch {
throw "Failed to install NSSM: $_"
}
}
function New-DirectoryStructure {
Write-Step "Creating directory structure..."
$directories = @(
$Config.InstallPath,
(Join-Path $Config.InstallPath "app"),
$Config.DataPath,
$Config.LogsPath,
$Config.TempPath,
$Config.BackupPath
)
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 (service needs full access to data, logs, backups)
try {
$acl = Get-Acl $Config.InstallPath
# Grant SYSTEM full control
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
)
$acl.SetAccessRule($systemRule)
# Grant Administrators full control
$adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
)
$acl.SetAccessRule($adminRule)
Set-Acl -Path $Config.InstallPath -AclObject $acl
Write-Success "Permissions set for SYSTEM and Administrators"
} catch {
Write-Warning "Could not set permissions: $_"
}
}
function Install-PythonDependencies {
Write-Step "Installing Python dependencies..."
$requirementsPath = Join-Path $Config.InstallPath "requirements.txt"
if (-not (Test-Path $requirementsPath)) {
Write-Warning "requirements.txt not found at $requirementsPath"
Write-Warning "Please copy application files first, then run: pip install -r requirements.txt"
return
}
try {
# Create virtual environment
$venvPath = Join-Path $Config.InstallPath "venv"
if (-not (Test-Path $venvPath)) {
Write-Step "Creating virtual environment..."
& python -m venv $venvPath
Write-Success "Virtual environment created"
} else {
Write-Success "Virtual environment already exists"
}
# Activate and install dependencies
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
Write-Step "Upgrading pip..."
& $pythonPath -m pip install --upgrade pip
Write-Step "Installing dependencies..."
& $pipPath install -r $requirementsPath
Write-Success "Python dependencies installed successfully"
} catch {
throw "Failed to install Python dependencies: $_"
}
}
function New-WindowsService {
Write-Step "Creating Windows Service for Telegram bot..."
# Check if service already exists
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
$nssmOutput = & nssm status $Config.ServiceName 2>&1
$serviceExists = $LASTEXITCODE -eq 0
$ErrorActionPreference = $oldErrorAction
if ($serviceExists) {
Write-Warning "Service already exists, stopping and removing..."
& nssm stop $Config.ServiceName 2>&1 | Out-Null
Start-Sleep -Seconds 2
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
Start-Sleep -Seconds 2
Write-Success "Existing service removed"
}
# Get Python path from virtual environment
$venvPath = Join-Path $Config.InstallPath "venv"
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
if (-not (Test-Path $pythonPath)) {
throw "Virtual environment not found. Please run Install-PythonDependencies first."
}
$appModule = "-m"
$appMain = "app.main"
# NSSM service creation
try {
# Install service
& nssm install $Config.ServiceName $pythonPath $appModule $appMain
# Set service configuration
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
& nssm set $Config.ServiceName Description $Config.ServiceDescription
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
& nssm set $Config.ServiceName AppDirectory $Config.InstallPath
# Set environment variables
$envFile = Join-Path $Config.InstallPath ".env"
if (Test-Path $envFile) {
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$($Config.InstallPath)"
Write-Success ".env file will be loaded by application"
} else {
Write-Warning ".env file not found - create it before starting service"
}
# Set logging
$stdoutLog = Join-Path $Config.LogsPath "stdout.log"
$stderrLog = Join-Path $Config.LogsPath "stderr.log"
& nssm set $Config.ServiceName AppStdout $stdoutLog
& nssm set $Config.ServiceName AppStderr $stderrLog
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
# Set restart policy (important for bot reliability)
& nssm set $Config.ServiceName AppExit Default Restart
& nssm set $Config.ServiceName AppRestartDelay 5000
& nssm set $Config.ServiceName AppThrottle 10000
Write-Success "Windows Service created successfully"
} catch {
throw "Failed to create Windows Service: $_"
}
}
function New-ConfigurationFile {
Write-Step "Creating configuration template..."
$envExample = Join-Path $Config.InstallPath ".env.example"
$envFile = Join-Path $Config.InstallPath ".env"
# Create .env.example template
$envTemplate = @"
# ROA2WEB Telegram Bot - Production Configuration
# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_production_bot_token_here
# Claude Authentication Configuration
# =====================================
# Two authentication methods are supported:
#
# Method 1: Claude Pro/Max Subscription (RECOMMENDED)
# - Leave CLAUDE_API_KEY empty or remove the line
# - Run: scripts\Setup-ClaudeAuth.ps1
# - Authenticate via browser with your Claude Pro/Max account
# - No additional costs!
#
# Method 2: Claude API Key (Alternative)
# - Get API key from: https://console.anthropic.com/settings/keys
# - Set CLAUDE_API_KEY below
# - This will take precedence over browser login
# - Usage-based billing applies
#
# Leave empty to use Claude Pro/Max subscription:
CLAUDE_API_KEY=
# Backend API Configuration
BACKEND_URL=http://localhost:8000
BACKEND_TIMEOUT=30
# SQLite Database Configuration
SQLITE_DB_PATH=$($Config.DataPath -replace '\\', '\\')\telegram_bot.db
# Internal API Configuration (for backend callbacks)
INTERNAL_API_HOST=127.0.0.1
INTERNAL_API_PORT=$($Config.ServicePort)
# Logging Configuration
LOG_LEVEL=INFO
LOG_FILE=$($Config.LogsPath -replace '\\', '\\')\bot.log
# Environment
ENVIRONMENT=production
# Authentication Configuration
AUTH_CODE_EXPIRY_MINUTES=15
JWT_REFRESH_THRESHOLD_MINUTES=5
# Session Configuration
SESSION_TIMEOUT_MINUTES=60
MAX_CONVERSATION_HISTORY=20
"@
# Write .env.example
Set-Content -Path $envExample -Value $envTemplate -Encoding UTF8
Write-Success "Created .env.example template"
# Copy to .env if it doesn't exist
if (-not (Test-Path $envFile)) {
Copy-Item -Path $envExample -Destination $envFile
Write-Warning "Created .env file - PLEASE UPDATE WITH PRODUCTION VALUES"
Write-Host " Edit: $envFile" -ForegroundColor Yellow
} else {
Write-Success ".env file already exists (not overwriting)"
}
}
function Test-ServiceHealth {
Write-Step "Testing service health..."
Start-Sleep -Seconds 5
try {
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/internal/health" -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
$content = $response.Content | ConvertFrom-Json
Write-Success "Health check passed: $($content.status)"
return $true
}
} catch {
Write-Warning "Health check failed (service may need configuration): $_"
return $false
}
}
function Show-Summary {
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host " ROA2WEB TELEGRAM BOT INSTALLATION COMPLETED" -ForegroundColor Green
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
Write-Host " Source Path: $($Config.SourcePath)"
Write-Host " Install Path: $($Config.InstallPath)"
Write-Host " Scripts Path: $($Config.InstallPath)\scripts\"
Write-Host " Data Path: $($Config.DataPath)"
Write-Host " Logs Path: $($Config.LogsPath)"
Write-Host " Backup Path: $($Config.BackupPath)"
Write-Host " Service Name: $($Config.ServiceName)"
Write-Host " Internal API Port: $($Config.ServicePort)"
Write-Host "`nService Endpoints:" -ForegroundColor Yellow
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/internal/health"
Write-Host " Stats: http://localhost:$($Config.ServicePort)/internal/stats"
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host " 1. Edit configuration: $($Config.InstallPath)\.env"
Write-Host " - Set TELEGRAM_BOT_TOKEN (from @BotFather)"
Write-Host " - Set CLAUDE_API_KEY (from Anthropic console)"
Write-Host " - Verify BACKEND_URL=http://localhost:8000"
Write-Host " 2. Navigate to scripts: cd $($Config.InstallPath)\scripts"
Write-Host " 3. Start service: .\Start-TelegramBot.ps1"
Write-Host " 4. Check logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50"
Write-Host "`nManagement Scripts Location:" -ForegroundColor Yellow
Write-Host " $($Config.InstallPath)\scripts\"
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
Write-Host " cd $($Config.InstallPath)\scripts"
Write-Host " .\Start-TelegramBot.ps1 # Start service"
Write-Host " .\Stop-TelegramBot.ps1 # Stop service"
Write-Host " .\Restart-TelegramBot.ps1 # Restart service"
Write-Host " .\Backup-TelegramDB.ps1 # Backup database"
Write-Host " .\Setup-DailyBackup.ps1 # Setup automated daily backups"
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
# =============================================================================
# MAIN INSTALLATION FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB Telegram Bot - Windows Server Installation Script
Telegram Bot Frontend with Claude Agent SDK
====================================================================
"@ -ForegroundColor Cyan
# Check prerequisites
Write-Step "Checking prerequisites..."
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
exit 1
}
Write-Success "Running as Administrator"
try {
# Installation steps
Test-PythonInstallation
Install-NSSM
New-DirectoryStructure
# Copy application files from deployment package
$filesCopied = Copy-ApplicationFiles
if (-not $filesCopied) {
throw "Failed to copy application files. Please ensure you're running this script from the deployment package directory."
}
Install-PythonDependencies
New-ConfigurationFile
New-WindowsService
Show-Summary
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
Write-Host "IMPORTANT: Configure .env file before starting service" -ForegroundColor Yellow
} catch {
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
exit 1
}
}
# Run main installation
Main

View File

@@ -0,0 +1,38 @@
<#
.SYNOPSIS
Restart ROA2WEB Backend Service
.DESCRIPTION
Stops and starts the ROA2WEB backend Windows service.
.EXAMPLE
.\Restart-ROA2WEB.ps1
#>
[CmdletBinding()]
param(
[string]$ServiceName = "ROA2WEB-Backend"
)
$ErrorActionPreference = "Stop"
Write-Host "`n[*] Restarting ROA2WEB Backend Service..." -ForegroundColor Cyan
try {
# Stop service
Write-Host "`n[*] Stopping service..." -ForegroundColor Yellow
& "$PSScriptRoot\Stop-ROA2WEB.ps1" -ServiceName $ServiceName
# Wait a moment
Start-Sleep -Seconds 2
# Start service
Write-Host "`n[*] Starting service..." -ForegroundColor Yellow
& "$PSScriptRoot\Start-ROA2WEB.ps1" -ServiceName $ServiceName
Write-Host "`n[OK] Service restarted successfully" -ForegroundColor Green
exit 0
} catch {
Write-Host "`n[ERROR] Failed to restart service: $_" -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,38 @@
<#
.SYNOPSIS
Restart ROA2WEB Telegram Bot Service
.DESCRIPTION
Stops and starts the ROA2WEB Telegram Bot Windows service.
.EXAMPLE
.\Restart-TelegramBot.ps1
#>
[CmdletBinding()]
param(
[string]$ServiceName = "ROA2WEB-TelegramBot"
)
$ErrorActionPreference = "Stop"
Write-Host "`n[*] Restarting ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
try {
# Stop service
Write-Host "`n[*] Stopping service..." -ForegroundColor Yellow
& "$PSScriptRoot\Stop-TelegramBot.ps1" -ServiceName $ServiceName
# Wait a moment
Start-Sleep -Seconds 2
# Start service
Write-Host "`n[*] Starting service..." -ForegroundColor Yellow
& "$PSScriptRoot\Start-TelegramBot.ps1" -ServiceName $ServiceName
Write-Host "`n[OK] Service restarted successfully" -ForegroundColor Green
exit 0
} catch {
Write-Host "`n[ERROR] Failed to restart service: $_" -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,365 @@
<#
.SYNOPSIS
Setup Daily Backup Task for ROA2WEB Telegram Bot Database
.DESCRIPTION
This script configures Windows Task Scheduler to run daily database backups:
- Creates scheduled task (ROA2WEB-TelegramBot-Backup)
- Runs daily at specified time (default: 2:00 AM)
- Executes Backup-TelegramDB.ps1 script
- Runs as SYSTEM account
- Logs all backup operations
- Optional email notifications on failure
.PARAMETER BackupTime
Daily backup time in 24-hour format (default: "02:00")
.PARAMETER TaskName
Name of the scheduled task (default: ROA2WEB-TelegramBot-Backup)
.PARAMETER InstallPath
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
.PARAMETER RunAsUser
User account to run task (default: SYSTEM)
.PARAMETER EnableEmailAlerts
Enable email notifications on backup failure (default: false)
.EXAMPLE
.\Setup-DailyBackup.ps1
Setup daily backup at 2:00 AM
.EXAMPLE
.\Setup-DailyBackup.ps1 -BackupTime "03:30"
Setup daily backup at 3:30 AM
.EXAMPLE
.\Setup-DailyBackup.ps1 -EnableEmailAlerts $true
Setup with email notifications on failure
.NOTES
Author: ROA2WEB Team
Requires: PowerShell 5.1+, Administrator privileges
#>
[CmdletBinding()]
param(
[string]$BackupTime = "02:00",
[string]$TaskName = "ROA2WEB-TelegramBot-Backup",
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
[string]$RunAsUser = "SYSTEM",
[bool]$EnableEmailAlerts = $false
)
$ErrorActionPreference = "Stop"
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
function Write-Step {
param([string]$Message)
Write-Host "`n[*] $Message" -ForegroundColor Cyan
}
function Write-Success {
param([string]$Message)
Write-Host " [OK] $Message" -ForegroundColor Green
}
function Write-Error {
param([string]$Message)
Write-Host " [ERROR] $Message" -ForegroundColor Red
}
function Write-Warning {
param([string]$Message)
Write-Host " [WARN] $Message" -ForegroundColor Yellow
}
function Test-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-BackupScript {
$backupScriptPath = Join-Path $PSScriptRoot "Backup-TelegramDB.ps1"
if (-not (Test-Path $backupScriptPath)) {
throw "Backup script not found: $backupScriptPath"
}
Write-Success "Backup script found: $backupScriptPath"
return $backupScriptPath
}
function Remove-ExistingTask {
param([string]$TaskName)
Write-Step "Checking for existing scheduled task..."
try {
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existingTask) {
Write-Warning "Existing task found, removing..."
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Success "Existing task removed"
} else {
Write-Success "No existing task found"
}
} catch {
Write-Warning "Could not check for existing task: $_"
}
}
function New-ScheduledBackupTask {
param(
[string]$TaskName,
[string]$BackupScriptPath,
[string]$BackupTime,
[string]$RunAsUser
)
Write-Step "Creating scheduled task..."
try {
# Parse backup time
$timeComponents = $BackupTime -split ":"
$hour = [int]$timeComponents[0]
$minute = [int]$timeComponents[1]
# Create task action (run PowerShell script)
$action = New-ScheduledTaskAction `
-Execute "PowerShell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$BackupScriptPath`""
# Create task trigger (daily at specified time)
$trigger = New-ScheduledTaskTrigger `
-Daily `
-At (Get-Date).Date.AddHours($hour).AddMinutes($minute)
# Create task settings
$settings = New-ScheduledTaskSettings `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RunOnlyIfNetworkAvailable:$false `
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
# Create task principal (run as specified user)
if ($RunAsUser -eq "SYSTEM") {
$principal = New-ScheduledTaskPrincipal `
-UserId "SYSTEM" `
-LogonType ServiceAccount `
-RunLevel Highest
} else {
$principal = New-ScheduledTaskPrincipal `
-UserId $RunAsUser `
-LogonType Password `
-RunLevel Highest
}
# Register task
$task = Register-ScheduledTask `
-TaskName $TaskName `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Principal $principal `
-Description "Daily backup of ROA2WEB Telegram Bot SQLite database"
Write-Success "Scheduled task created: $TaskName"
Write-Success "Schedule: Daily at $BackupTime"
Write-Success "Run as: $RunAsUser"
return $task
} catch {
throw "Failed to create scheduled task: $_"
}
}
function Test-TaskCreation {
param([string]$TaskName)
Write-Step "Verifying task creation..."
try {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
Write-Success "Task verified: $TaskName"
Write-Host " Task State: $($task.State)" -ForegroundColor Gray
Write-Host " Last Run: $($taskInfo.LastRunTime)" -ForegroundColor Gray
Write-Host " Last Result: $($taskInfo.LastTaskResult)" -ForegroundColor Gray
Write-Host " Next Run: $($taskInfo.NextRunTime)" -ForegroundColor Gray
return $true
} catch {
Write-Error "Task verification failed: $_"
return $false
}
}
function Test-TaskExecution {
param([string]$TaskName)
Write-Step "Testing task execution..."
try {
# Run task immediately
Start-ScheduledTask -TaskName $TaskName
Write-Success "Task execution started"
Write-Host " Waiting for task to complete..." -ForegroundColor Yellow
# Wait for task to complete (max 60 seconds)
$timeout = 60
$elapsed = 0
while ($elapsed -lt $timeout) {
Start-Sleep -Seconds 2
$elapsed += 2
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
$task = Get-ScheduledTask -TaskName $TaskName
if ($task.State -ne "Running") {
break
}
Write-Host " Task still running... ($elapsed/$timeout seconds)" -ForegroundColor Yellow
}
# Check result
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
if ($taskInfo.LastTaskResult -eq 0) {
Write-Success "Task executed successfully"
Write-Success "Last run: $($taskInfo.LastRunTime)"
return $true
} else {
Write-Warning "Task completed with result code: $($taskInfo.LastTaskResult)"
Write-Host " Check backup logs for details" -ForegroundColor Yellow
return $false
}
} catch {
Write-Error "Task execution test failed: $_"
return $false
}
}
function Show-TaskSummary {
param([string]$TaskName, [string]$BackupTime, [string]$InstallPath)
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host " DAILY BACKUP TASK CONFIGURED SUCCESSFULLY" -ForegroundColor Green
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "`nTask Configuration:" -ForegroundColor Yellow
Write-Host " Task Name: $TaskName"
Write-Host " Schedule: Daily at $BackupTime"
Write-Host " Run As: $RunAsUser"
Write-Host " Installation Path: $InstallPath"
$task = Get-ScheduledTask -TaskName $TaskName
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
Write-Host "`nTask Status:" -ForegroundColor Yellow
Write-Host " State: $($task.State)"
Write-Host " Last Run: $($taskInfo.LastRunTime)"
Write-Host " Last Result: $($taskInfo.LastTaskResult)"
Write-Host " Next Run: $($taskInfo.NextRunTime)"
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
Write-Host " View task: Get-ScheduledTask -TaskName '$TaskName'"
Write-Host " Run manually: Start-ScheduledTask -TaskName '$TaskName'"
Write-Host " Disable task: Disable-ScheduledTask -TaskName '$TaskName'"
Write-Host " Enable task: Enable-ScheduledTask -TaskName '$TaskName'"
Write-Host " Remove task: Unregister-ScheduledTask -TaskName '$TaskName'"
Write-Host "`nBackup Management:" -ForegroundColor Yellow
Write-Host " Manual backup: .\Backup-TelegramDB.ps1"
Write-Host " Backup logs: Get-Content $InstallPath\logs\backup.log -Tail 50"
Write-Host " Backups location: $InstallPath\backups"
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
}
# =============================================================================
# MAIN SETUP FLOW
# =============================================================================
function Main {
Write-Host @"
====================================================================
ROA2WEB Telegram Bot - Setup Daily Backup Task
Configure automated database backup via Task Scheduler
====================================================================
"@ -ForegroundColor Cyan
# Check prerequisites
Write-Step "Checking prerequisites..."
if (-not (Test-Administrator)) {
Write-Error "This script must be run as Administrator"
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
exit 1
}
Write-Success "Running as Administrator"
# Find backup script
$backupScriptPath = Test-BackupScript
# Validate installation path
if (-not (Test-Path $InstallPath)) {
Write-Error "Installation path not found: $InstallPath"
Write-Host " Run Install-TelegramBot.ps1 first" -ForegroundColor Yellow
exit 1
}
Write-Success "Installation path verified"
try {
# Setup task
Remove-ExistingTask -TaskName $TaskName
$task = New-ScheduledBackupTask `
-TaskName $TaskName `
-BackupScriptPath $backupScriptPath `
-BackupTime $BackupTime `
-RunAsUser $RunAsUser
# Verify task creation
$verified = Test-TaskCreation -TaskName $TaskName
if ($verified) {
# Test task execution
Write-Host "`nDo you want to test the backup task now? (Y/N)" -ForegroundColor Yellow
$response = Read-Host
if ($response -eq "Y" -or $response -eq "y") {
Test-TaskExecution -TaskName $TaskName
} else {
Write-Host " Skipping test execution" -ForegroundColor Gray
}
# Show summary
Show-TaskSummary -TaskName $TaskName -BackupTime $BackupTime -InstallPath $InstallPath
Write-Host "`nSetup completed successfully!" -ForegroundColor Green
exit 0
} else {
throw "Task verification failed"
}
} catch {
Write-Host "`n[SETUP FAILED] $_" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Red
exit 1
}
}
# Run main setup
Main

View File

@@ -0,0 +1,66 @@
<#
.SYNOPSIS
Start ROA2WEB Backend Service
.DESCRIPTION
Starts the ROA2WEB backend Windows service and validates it's running properly.
.EXAMPLE
.\Start-ROA2WEB.ps1
#>
[CmdletBinding()]
param(
[string]$ServiceName = "ROA2WEB-Backend",
[int]$Timeout = 30
)
$ErrorActionPreference = "Stop"
Write-Host "`n[*] Starting ROA2WEB Backend Service..." -ForegroundColor Cyan
try {
$service = Get-Service -Name $ServiceName -ErrorAction Stop
if ($service.Status -eq "Running") {
Write-Host " [OK] Service is already running" -ForegroundColor Green
exit 0
}
# Start service
Start-Service -Name $ServiceName
Write-Host " [*] Service start command issued" -ForegroundColor Yellow
# Wait for service to start
$elapsed = 0
while ($service.Status -ne "Running" -and $elapsed -lt $Timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
Write-Host " [*] Waiting for service to start... ($elapsed/$Timeout)" -ForegroundColor Yellow
}
if ($service.Status -eq "Running") {
Write-Host " [OK] Service started successfully" -ForegroundColor Green
# Wait a bit and test health endpoint
Start-Sleep -Seconds 3
try {
$response = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5
if ($response.StatusCode -eq 200) {
Write-Host " [OK] Health check passed" -ForegroundColor Green
}
} catch {
Write-Host " [WARN] Health check failed (service may still be starting)" -ForegroundColor Yellow
}
exit 0
} else {
Write-Host " [ERROR] Service failed to start (Status: $($service.Status))" -ForegroundColor Red
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log" -ForegroundColor Yellow
exit 1
}
} catch {
Write-Host " [ERROR] Failed to start service: $_" -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,70 @@
<#
.SYNOPSIS
Start ROA2WEB Telegram Bot Service
.DESCRIPTION
Starts the ROA2WEB Telegram Bot Windows service and validates it's running properly.
.EXAMPLE
.\Start-TelegramBot.ps1
#>
[CmdletBinding()]
param(
[string]$ServiceName = "ROA2WEB-TelegramBot",
[int]$Timeout = 30,
[int]$HealthPort = 8002
)
$ErrorActionPreference = "Stop"
Write-Host "`n[*] Starting ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
try {
$service = Get-Service -Name $ServiceName -ErrorAction Stop
if ($service.Status -eq "Running") {
Write-Host " [OK] Service is already running" -ForegroundColor Green
exit 0
}
# Start service
Start-Service -Name $ServiceName
Write-Host " [*] Service start command issued" -ForegroundColor Yellow
# Wait for service to start
$elapsed = 0
while ($service.Status -ne "Running" -and $elapsed -lt $Timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
Write-Host " [*] Waiting for service to start... ($elapsed/$Timeout)" -ForegroundColor Yellow
}
if ($service.Status -eq "Running") {
Write-Host " [OK] Service started successfully" -ForegroundColor Green
# Wait a bit and test health endpoint
Start-Sleep -Seconds 5
try {
$response = Invoke-WebRequest -Uri "http://localhost:$HealthPort/internal/health" -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
$content = $response.Content | ConvertFrom-Json
Write-Host " [OK] Health check passed: $($content.status)" -ForegroundColor Green
Write-Host " [OK] Database: $($content.database.status)" -ForegroundColor Green
}
} catch {
Write-Host " [WARN] Health check failed (service may still be starting): $_" -ForegroundColor Yellow
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log" -ForegroundColor Yellow
}
exit 0
} else {
Write-Host " [ERROR] Service failed to start (Status: $($service.Status))" -ForegroundColor Red
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log" -ForegroundColor Yellow
exit 1
}
} catch {
Write-Host " [ERROR] Failed to start service: $_" -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,54 @@
<#
.SYNOPSIS
Stop ROA2WEB Backend Service
.DESCRIPTION
Stops the ROA2WEB backend Windows service gracefully.
.EXAMPLE
.\Stop-ROA2WEB.ps1
#>
[CmdletBinding()]
param(
[string]$ServiceName = "ROA2WEB-Backend",
[int]$Timeout = 30
)
$ErrorActionPreference = "Stop"
Write-Host "`n[*] Stopping ROA2WEB Backend Service..." -ForegroundColor Cyan
try {
$service = Get-Service -Name $ServiceName -ErrorAction Stop
if ($service.Status -eq "Stopped") {
Write-Host " [OK] Service is already stopped" -ForegroundColor Green
exit 0
}
# Stop service
Stop-Service -Name $ServiceName -Force
Write-Host " [*] Service stop command issued" -ForegroundColor Yellow
# Wait for service to stop
$elapsed = 0
while ($service.Status -ne "Stopped" -and $elapsed -lt $Timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
Write-Host " [*] Waiting for service to stop... ($elapsed/$Timeout)" -ForegroundColor Yellow
}
if ($service.Status -eq "Stopped") {
Write-Host " [OK] Service stopped successfully" -ForegroundColor Green
exit 0
} else {
Write-Host " [ERROR] Service did not stop within timeout (Status: $($service.Status))" -ForegroundColor Red
Write-Host " You may need to force kill the process" -ForegroundColor Yellow
exit 1
}
} catch {
Write-Host " [ERROR] Failed to stop service: $_" -ForegroundColor Red
exit 1
}

View File

@@ -0,0 +1,54 @@
<#
.SYNOPSIS
Stop ROA2WEB Telegram Bot Service
.DESCRIPTION
Stops the ROA2WEB Telegram Bot Windows service gracefully.
.EXAMPLE
.\Stop-TelegramBot.ps1
#>
[CmdletBinding()]
param(
[string]$ServiceName = "ROA2WEB-TelegramBot",
[int]$Timeout = 30
)
$ErrorActionPreference = "Stop"
Write-Host "`n[*] Stopping ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
try {
$service = Get-Service -Name $ServiceName -ErrorAction Stop
if ($service.Status -eq "Stopped") {
Write-Host " [OK] Service is already stopped" -ForegroundColor Green
exit 0
}
# Stop service
Stop-Service -Name $ServiceName -Force
Write-Host " [*] Service stop command issued" -ForegroundColor Yellow
# Wait for service to stop
$elapsed = 0
while ($service.Status -ne "Stopped" -and $elapsed -lt $Timeout) {
Start-Sleep -Seconds 1
$service.Refresh()
$elapsed++
Write-Host " [*] Waiting for service to stop... ($elapsed/$Timeout)" -ForegroundColor Yellow
}
if ($service.Status -eq "Stopped") {
Write-Host " [OK] Service stopped successfully" -ForegroundColor Green
exit 0
} else {
Write-Host " [ERROR] Service did not stop within timeout (Status: $($service.Status))" -ForegroundColor Red
Write-Host " You may need to force kill the process" -ForegroundColor Yellow
exit 1
}
} catch {
Write-Host " [ERROR] Failed to stop service: $_" -ForegroundColor Red
exit 1
}