- Add deployment/linux/ with deploy.sh for deploying from Claude-Agent LXC to Windows server - Add ServerLogsView.vue for viewing server logs from frontend - Add shared/routes/system.py for system health endpoints - Update CLAUDE.md with quick deploy instructions - Improve Windows deployment scripts (ROA2WEB-Console.ps1) - Fix OCR service validation and worker pool improvements - Update environment config examples - Various script permission and startup fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
548 lines
17 KiB
PowerShell
548 lines
17 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
ROA2WEB - Auto-Deploy Monitor (Server-Side) - Ultrathin Monolith
|
|
|
|
.DESCRIPTION
|
|
Server-side script that monitors C:\Temp\ for new deployment packages
|
|
and automatically deploys them using ROA2WEB-Console.ps1.
|
|
|
|
Designed for ULTRATHIN MONOLITH architecture:
|
|
- Single unified backend service (ROA2WEB-Backend)
|
|
- All modules deployed together (Reports, Data Entry, Telegram)
|
|
- Automatic detection and deployment
|
|
|
|
Can run:
|
|
- Via Scheduled Task (automated, silent)
|
|
- Interactive mode (manual execution with menu)
|
|
- Non-interactive mode (command-line automation)
|
|
|
|
.PARAMETER Interactive
|
|
Run in interactive mode with menu
|
|
|
|
.PARAMETER WatchPath
|
|
Path to monitor for deployment packages (default: C:\Temp)
|
|
|
|
.PARAMETER StateFile
|
|
Path to state file tracking last deployment (default: C:\Temp\ROA2WEB-Scripts\last-deploy.json)
|
|
|
|
.PARAMETER ConsoleScriptPath
|
|
Path to ROA2WEB-Console.ps1 script
|
|
|
|
.PARAMETER LogPath
|
|
Path to log file directory (default: C:\Temp\ROA2WEB-Scripts\Logs)
|
|
|
|
.PARAMETER CheckOnly
|
|
Check for updates without deploying
|
|
|
|
.EXAMPLE
|
|
.\Check-And-Deploy.ps1
|
|
Check for new packages and deploy automatically (silent)
|
|
|
|
.EXAMPLE
|
|
.\Check-And-Deploy.ps1 -Interactive
|
|
Show interactive menu with deployment options
|
|
|
|
.EXAMPLE
|
|
.\Check-And-Deploy.ps1 -CheckOnly
|
|
Check for new packages without deploying
|
|
|
|
.NOTES
|
|
Author: ROA2WEB Team
|
|
Version: 1.0 (Auto-Deploy Monitor)
|
|
Requires: Administrator privileges, PowerShell 5.1+
|
|
|
|
Designed to run via Scheduled Task for automated deployments.
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$Interactive,
|
|
[string]$WatchPath = "C:\Temp",
|
|
[string]$StateFile = "C:\Temp\ROA2WEB-Scripts\last-deploy.json",
|
|
[string]$ConsoleScriptPath = "",
|
|
[string]$LogPath = "C:\Temp\ROA2WEB-Scripts\Logs",
|
|
[switch]$CheckOnly
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# =============================================================================
|
|
# CONFIGURATION
|
|
# =============================================================================
|
|
|
|
$script:Config = @{
|
|
WatchPath = $WatchPath
|
|
StateFile = $StateFile
|
|
LogPath = $LogPath
|
|
ConsoleScriptPath = if ($ConsoleScriptPath) {
|
|
$ConsoleScriptPath
|
|
} else {
|
|
# Auto-detect ROA2WEB-Console.ps1
|
|
$possiblePaths = @(
|
|
"C:\inetpub\wwwroot\roa2web\scripts\ROA2WEB-Console.ps1",
|
|
(Join-Path $PSScriptRoot "ROA2WEB-Console.ps1")
|
|
)
|
|
$found = $possiblePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
if ($found) { $found } else { "" }
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# HELPER FUNCTIONS
|
|
# =============================================================================
|
|
|
|
function Write-Log {
|
|
param(
|
|
[string]$Message,
|
|
[ValidateSet("INFO", "SUCCESS", "WARNING", "ERROR")]
|
|
[string]$Level = "INFO"
|
|
)
|
|
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
$logMessage = "[$timestamp] [$Level] $Message"
|
|
|
|
# Ensure log directory exists
|
|
if (-not (Test-Path $script:Config.LogPath)) {
|
|
New-Item -ItemType Directory -Path $script:Config.LogPath -Force | Out-Null
|
|
}
|
|
|
|
$logFile = Join-Path $script:Config.LogPath "check-and-deploy.log"
|
|
Add-Content -Path $logFile -Value $logMessage
|
|
|
|
# Also write to console if interactive
|
|
if ($Interactive) {
|
|
$color = switch ($Level) {
|
|
"SUCCESS" { "Green" }
|
|
"WARNING" { "Yellow" }
|
|
"ERROR" { "Red" }
|
|
default { "White" }
|
|
}
|
|
Write-Host $logMessage -ForegroundColor $color
|
|
}
|
|
}
|
|
|
|
function Write-Step {
|
|
param([string]$Message)
|
|
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
|
Write-Log -Message $Message -Level "INFO"
|
|
}
|
|
|
|
function Write-Success {
|
|
param([string]$Message)
|
|
Write-Host " [OK] $Message" -ForegroundColor Green
|
|
Write-Log -Message $Message -Level "SUCCESS"
|
|
}
|
|
|
|
function Write-Error {
|
|
param([string]$Message)
|
|
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
|
Write-Log -Message $Message -Level "ERROR"
|
|
}
|
|
|
|
function Write-Warning {
|
|
param([string]$Message)
|
|
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
|
Write-Log -Message $Message -Level "WARNING"
|
|
}
|
|
|
|
function Wait-ForKeyPress {
|
|
Write-Host "`nPress any key to continue..." -ForegroundColor Gray
|
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
}
|
|
|
|
# =============================================================================
|
|
# STATE MANAGEMENT
|
|
# =============================================================================
|
|
|
|
function Get-DeploymentState {
|
|
if (Test-Path $script:Config.StateFile) {
|
|
try {
|
|
return Get-Content -Path $script:Config.StateFile -Raw | ConvertFrom-Json
|
|
} catch {
|
|
Write-Warning "Failed to load state file, starting fresh"
|
|
return $null
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Save-DeploymentState {
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$DeploymentInfo
|
|
)
|
|
|
|
try {
|
|
# Load existing state
|
|
$state = Get-DeploymentState
|
|
|
|
$newEntry = @{
|
|
packageName = $DeploymentInfo.PackageName
|
|
component = $DeploymentInfo.Component
|
|
timestamp = (Get-Date).ToUniversalTime().ToString("o")
|
|
status = $DeploymentInfo.Status
|
|
services = $DeploymentInfo.Services
|
|
}
|
|
|
|
if ($state) {
|
|
# Add current deployment to history
|
|
if (-not $state.history) {
|
|
$state.history = @()
|
|
}
|
|
if ($state.lastDeployment) {
|
|
$state.history = @($state.lastDeployment) + $state.history
|
|
}
|
|
|
|
# Keep only last 10 entries in history
|
|
if ($state.history.Count -gt 10) {
|
|
$state.history = $state.history | Select-Object -First 10
|
|
}
|
|
|
|
$state.lastDeployment = $newEntry
|
|
} else {
|
|
$state = @{
|
|
lastDeployment = $newEntry
|
|
history = @()
|
|
}
|
|
}
|
|
|
|
# Ensure directory exists
|
|
$stateDir = Split-Path $script:Config.StateFile -Parent
|
|
if (-not (Test-Path $stateDir)) {
|
|
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
}
|
|
|
|
# Save state
|
|
$state | ConvertTo-Json -Depth 10 | Set-Content -Path $script:Config.StateFile
|
|
Write-Success "Deployment state saved"
|
|
} catch {
|
|
Write-Error "Failed to save deployment state: $_"
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# PACKAGE DETECTION
|
|
# =============================================================================
|
|
|
|
function Get-LatestDeploymentPackage {
|
|
Write-Step "Scanning for deployment packages..."
|
|
|
|
if (-not (Test-Path $script:Config.WatchPath)) {
|
|
Write-Warning "Watch path does not exist: $($script:Config.WatchPath)"
|
|
return $null
|
|
}
|
|
|
|
# Find all deploy-* folders
|
|
$packages = Get-ChildItem -Path $script:Config.WatchPath -Directory |
|
|
Where-Object { $_.Name -match "^deploy-\d{8}-\d{6}$" } |
|
|
Sort-Object LastWriteTime -Descending
|
|
|
|
if ($packages.Count -eq 0) {
|
|
Write-Warning "No deployment packages found"
|
|
return $null
|
|
}
|
|
|
|
$latest = $packages | Select-Object -First 1
|
|
Write-Success "Found $($packages.Count) package(s), latest: $($latest.Name)"
|
|
|
|
return $latest
|
|
}
|
|
|
|
function Test-IsNewPackage {
|
|
param([Parameter(Mandatory)]$Package)
|
|
|
|
$state = Get-DeploymentState
|
|
|
|
if (-not $state -or -not $state.lastDeployment) {
|
|
Write-Success "No previous deployment found - this is a new package"
|
|
return $true
|
|
}
|
|
|
|
$lastDeployed = $state.lastDeployment.packageName
|
|
|
|
if ($Package.Name -ne $lastDeployed) {
|
|
Write-Success "New package detected (last deployed: $lastDeployed)"
|
|
return $true
|
|
}
|
|
|
|
Write-Log -Message "Package already deployed: $($Package.Name)" -Level "INFO"
|
|
return $false
|
|
}
|
|
|
|
# =============================================================================
|
|
# DEPLOYMENT EXECUTION
|
|
# =============================================================================
|
|
|
|
function Invoke-Deployment {
|
|
param([Parameter(Mandatory)]$Package)
|
|
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " STARTING DEPLOYMENT" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host "`nPackage: $($Package.Name)" -ForegroundColor Yellow
|
|
Write-Host "Path: $($Package.FullName)" -ForegroundColor Gray
|
|
|
|
# Find ROA2WEB-Console.ps1 in package
|
|
$consoleScript = Join-Path $Package.FullName "scripts\ROA2WEB-Console.ps1"
|
|
|
|
if (-not (Test-Path $consoleScript)) {
|
|
Write-Error "ROA2WEB-Console.ps1 not found in package: $consoleScript"
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
Write-Step "Executing deployment via ROA2WEB-Console.ps1..."
|
|
|
|
# Execute deployment (deploy all components)
|
|
Push-Location (Split-Path $consoleScript -Parent)
|
|
|
|
& $consoleScript -NonInteractive -Action DeployAll -PackagePath $Package.FullName
|
|
|
|
# Capture exit code IMMEDIATELY (before any other command that might reset it)
|
|
$exitCode = $LASTEXITCODE
|
|
|
|
# Run OCR dependency check with auto-install
|
|
Write-Log -Message "Checking and installing OCR dependencies..." -Level "INFO"
|
|
& $consoleScript -NonInteractive -Action InstallOCR 2>&1 | ForEach-Object { Write-Log -Message $_ -Level "INFO" }
|
|
|
|
Pop-Location
|
|
|
|
# Check if exit code indicates success (0 = success)
|
|
$deploySuccess = ($exitCode -eq 0)
|
|
|
|
if ($deploySuccess) {
|
|
Write-Success "Deployment completed successfully (exit code: $exitCode)"
|
|
|
|
# Save deployment state
|
|
Save-DeploymentState -DeploymentInfo @{
|
|
PackageName = $Package.Name
|
|
Component = "All"
|
|
Status = "Success"
|
|
Services = @{
|
|
backend = "Running (Ultrathin Monolith - All modules)"
|
|
}
|
|
}
|
|
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Green
|
|
Write-Host " DEPLOYMENT SUCCESSFUL" -ForegroundColor Green
|
|
Write-Host ("=" * 70) -ForegroundColor Green
|
|
|
|
return $true
|
|
} else {
|
|
Write-Error "Deployment failed (exit code: $exitCode)"
|
|
|
|
# Save failure state
|
|
Save-DeploymentState -DeploymentInfo @{
|
|
PackageName = $Package.Name
|
|
Component = "All"
|
|
Status = "Failed"
|
|
Services = @{}
|
|
}
|
|
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Red
|
|
Write-Host " DEPLOYMENT FAILED" -ForegroundColor Red
|
|
Write-Host ("=" * 70) -ForegroundColor Red
|
|
|
|
return $false
|
|
}
|
|
} catch {
|
|
Write-Error "Deployment exception: $_"
|
|
Write-Log -Message $_.ScriptStackTrace -Level "ERROR"
|
|
|
|
# Save failure state
|
|
Save-DeploymentState -DeploymentInfo @{
|
|
PackageName = $Package.Name
|
|
Component = "All"
|
|
Status = "Failed"
|
|
Services = @{}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# INTERACTIVE MENU
|
|
# =============================================================================
|
|
|
|
function Show-MainMenu {
|
|
Clear-Host
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " ROA2WEB - Auto-Deploy Monitor (Interactive)" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " [1] Check for Updates (No Deploy)" -ForegroundColor White
|
|
Write-Host " (Scan for new packages without deploying)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " [2] Check and Deploy Now" -ForegroundColor White
|
|
Write-Host " (Find latest package and deploy if new)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " [3] View Deployment History" -ForegroundColor White
|
|
Write-Host " (Show previous deployments)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " [4] View Current Configuration" -ForegroundColor White
|
|
Write-Host " (Display monitor settings)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host " [Q] Quit" -ForegroundColor Red
|
|
Write-Host ""
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
|
|
do {
|
|
Write-Host "`nYour choice: " -ForegroundColor Yellow -NoNewline
|
|
$choice = Read-Host
|
|
|
|
switch ($choice.ToUpper()) {
|
|
"1" { return "CheckOnly" }
|
|
"2" { return "CheckAndDeploy" }
|
|
"3" { return "History" }
|
|
"4" { return "ViewConfig" }
|
|
"Q" { return "Quit" }
|
|
default {
|
|
Write-Host "Invalid choice. Please select 1-4 or Q." -ForegroundColor Red
|
|
}
|
|
}
|
|
} while ($true)
|
|
}
|
|
|
|
function Show-DeploymentHistory {
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " Deployment History" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
|
|
$state = Get-DeploymentState
|
|
|
|
if (-not $state) {
|
|
Write-Host "`nNo deployment history found" -ForegroundColor Yellow
|
|
return
|
|
}
|
|
|
|
if ($state.lastDeployment) {
|
|
Write-Host "`nLast Deployment:" -ForegroundColor Yellow
|
|
Write-Host " Package: $($state.lastDeployment.packageName)" -ForegroundColor Gray
|
|
Write-Host " Component: $($state.lastDeployment.component)" -ForegroundColor Gray
|
|
Write-Host " Timestamp: $($state.lastDeployment.timestamp)" -ForegroundColor Gray
|
|
Write-Host " Status: $($state.lastDeployment.status)" -ForegroundColor $(if ($state.lastDeployment.status -eq "Success") { "Green" } else { "Red" })
|
|
}
|
|
|
|
if ($state.history -and $state.history.Count -gt 0) {
|
|
Write-Host "`nPrevious Deployments:" -ForegroundColor Yellow
|
|
$i = 1
|
|
foreach ($entry in $state.history) {
|
|
Write-Host " [$i] $($entry.packageName) - $($entry.timestamp) - $($entry.status)" -ForegroundColor Gray
|
|
$i++
|
|
}
|
|
}
|
|
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
}
|
|
|
|
function Show-Configuration {
|
|
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host " Current Configuration" -ForegroundColor Cyan
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host " Watch Path: $($script:Config.WatchPath)" -ForegroundColor Gray
|
|
Write-Host " State File: $($script:Config.StateFile)" -ForegroundColor Gray
|
|
Write-Host " Log Path: $($script:Config.LogPath)" -ForegroundColor Gray
|
|
Write-Host " Console Script: $($script:Config.ConsoleScriptPath)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
Write-Host ("=" * 70) -ForegroundColor Cyan
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN CHECK & DEPLOY LOGIC
|
|
# =============================================================================
|
|
|
|
function Invoke-CheckAndDeploy {
|
|
param([switch]$CheckOnly)
|
|
|
|
Write-Log -Message "=== Check and Deploy Started ===" -Level "INFO"
|
|
|
|
# Get latest package
|
|
$latestPackage = Get-LatestDeploymentPackage
|
|
|
|
if (-not $latestPackage) {
|
|
Write-Log -Message "No deployment packages found" -Level "INFO"
|
|
return $false
|
|
}
|
|
|
|
# Check if it's a new package
|
|
$isNew = Test-IsNewPackage -Package $latestPackage
|
|
|
|
if (-not $isNew) {
|
|
Write-Log -Message "No new packages to deploy" -Level "INFO"
|
|
return $false
|
|
}
|
|
|
|
if ($CheckOnly) {
|
|
Write-Success "New package available: $($latestPackage.Name)"
|
|
Write-Host "`nRun without -CheckOnly to deploy" -ForegroundColor Yellow
|
|
return $false
|
|
}
|
|
|
|
# Deploy the package
|
|
$deploySuccess = Invoke-Deployment -Package $latestPackage
|
|
|
|
Write-Log -Message "=== Check and Deploy Completed (Success: $deploySuccess) ===" -Level "INFO"
|
|
|
|
return $deploySuccess
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN EXECUTION FLOW
|
|
# =============================================================================
|
|
|
|
function Main {
|
|
# Check if running as Administrator
|
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
|
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
|
|
if (-not $isAdmin) {
|
|
Write-Host "`n[ERROR] This script requires Administrator privileges" -ForegroundColor Red
|
|
Write-Host "Please run PowerShell as Administrator and try again.`n" -ForegroundColor Yellow
|
|
exit 1
|
|
}
|
|
|
|
# Interactive mode
|
|
if ($Interactive) {
|
|
do {
|
|
$mainChoice = Show-MainMenu
|
|
|
|
switch ($mainChoice) {
|
|
"CheckOnly" {
|
|
Invoke-CheckAndDeploy -CheckOnly
|
|
Wait-ForKeyPress
|
|
}
|
|
|
|
"CheckAndDeploy" {
|
|
Invoke-CheckAndDeploy
|
|
Wait-ForKeyPress
|
|
}
|
|
|
|
"History" {
|
|
Show-DeploymentHistory
|
|
Wait-ForKeyPress
|
|
}
|
|
|
|
"ViewConfig" {
|
|
Show-Configuration
|
|
Wait-ForKeyPress
|
|
}
|
|
|
|
"Quit" {
|
|
Write-Host "`nGoodbye!`n" -ForegroundColor Cyan
|
|
return
|
|
}
|
|
}
|
|
} while ($true)
|
|
}
|
|
# Non-interactive mode (for Scheduled Task)
|
|
else {
|
|
Invoke-CheckAndDeploy -CheckOnly:$CheckOnly | Out-Null
|
|
}
|
|
}
|
|
|
|
# Run main
|
|
Main
|