[CmdletBinding()] param( [string]$StatusPath = (Join-Path $env:LOCALAPPDATA 'Domovoy\monitoring\status.json'), [string]$StatePath = (Join-Path $env:LOCALAPPDATA 'Domovoy\monitoring\notification-state.json'), [ValidateRange(1, 168)] [int]$ReminderHours = 6, [switch]$Force, [switch]$NoDisplay ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function Write-AtomicJson { param([Parameter(Mandatory)]$Value, [Parameter(Mandatory)][string]$Path) $directory = Split-Path -Parent $Path if (-not (Test-Path -LiteralPath $directory -PathType Container)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } $temporary = Join-Path $directory ('.{0}.{1}.tmp' -f (Split-Path -Leaf $Path), [guid]::NewGuid().ToString('N')) try { $Value | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $temporary -Encoding utf8NoBOM Move-Item -LiteralPath $temporary -Destination $Path -Force } finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force } } } if (-not (Test-Path -LiteralPath $StatusPath -PathType Leaf)) { throw "Health status not found: $StatusPath" } $status = Get-Content -LiteralPath $StatusPath -Raw | ConvertFrom-Json if ($status.version -ne 1 -or $status.overall -notin @('healthy', 'degraded')) { throw 'Unsupported health status document' } $now = [DateTimeOffset]::UtcNow $previous = $null if (Test-Path -LiteralPath $StatePath -PathType Leaf) { $previous = Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json } $kind = $null if ($Force) { $kind = if ($status.overall -eq 'healthy') { 'recovered' } else { 'degraded' } } elseif ($status.overall -eq 'degraded') { if ($null -eq $previous -or $previous.last_status -ne 'degraded') { $kind = 'degraded' } elseif ($null -ne $previous.last_notification_at_utc) { $lastNotification = if ($previous.last_notification_at_utc -is [DateTime]) { [DateTimeOffset]$previous.last_notification_at_utc } else { [DateTimeOffset]::Parse([string]$previous.last_notification_at_utc, [Globalization.CultureInfo]::InvariantCulture) } if (($now - $lastNotification).TotalHours -ge $ReminderHours) { $kind = 'reminder' } } } elseif ($null -ne $previous -and $previous.last_status -eq 'degraded') { $kind = 'recovered' } $lastNotificationAt = if ($null -ne $previous -and $null -ne $previous.last_notification_at_utc) { ([DateTimeOffset]$previous.last_notification_at_utc).ToString('o') } else { $null } $lastNotificationKind = if ($null -ne $previous) { $previous.last_notification_kind } else { $null } if ($null -ne $kind) { $failedNames = @($status.checks | Where-Object { -not $_.ok } | ForEach-Object { [string]$_.name }) switch ($kind) { 'degraded' { $title = 'Домовой: требуется внимание' $body = if ($failedNames.Count -gt 0) { 'Сбой: ' + ($failedNames -join ', ') } else { 'Мониторинг обнаружил сбой.' } $icon = 'Error' } 'reminder' { $title = 'Домовой: сбой продолжается' $body = if ($failedNames.Count -gt 0) { 'Не исправлено: ' + ($failedNames -join ', ') } else { 'Состояние остается ухудшенным.' } $icon = 'Warning' } default { $title = 'Домовой: работа восстановлена' $body = 'Все проверки снова проходят успешно.' $icon = 'Info' } } if (-not $NoDisplay) { Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $notification = [System.Windows.Forms.NotifyIcon]::new() try { $notification.Icon = switch ($icon) { 'Error' { [System.Drawing.SystemIcons]::Error } 'Warning' { [System.Drawing.SystemIcons]::Warning } default { [System.Drawing.SystemIcons]::Information } } $notification.Visible = $true $notification.BalloonTipIcon = [System.Enum]::Parse([System.Windows.Forms.ToolTipIcon], $icon) $notification.BalloonTipTitle = $title $notification.BalloonTipText = $body $notification.ShowBalloonTip(8000) Start-Sleep -Seconds 9 } finally { $notification.Dispose() } } $lastNotificationAt = $now.ToString('o') $lastNotificationKind = $kind } $state = [ordered]@{ version = 1 last_status = [string]$status.overall last_observed_at_utc = $now.ToString('o') last_notification_at_utc = $lastNotificationAt last_notification_kind = $lastNotificationKind } Write-AtomicJson -Value $state -Path $StatePath $result = if ($null -eq $kind) { 'suppressed' } elseif ($NoDisplay) { 'simulated' } else { 'sent' } Write-Output "notification=$result status=$($status.overall) kind=$kind"