Implement NotifyOnStatusChange feature for nodes and enhance Telegram notifications
CI / Backend (build + test) (push) Successful in 1m24s
CI / Frontend (lint + typecheck + build) (push) Successful in 34s

- Added `NotifyOnStatusChange` property to the `Node` class and related DTOs to allow individual node configuration for status change notifications.
- Updated `NodeHealthCheckService` to send Telegram notifications to admins when a node's status changes, based on the new property.
- Enhanced the `ITelegramNotifier` interface with a method for notifying admins about node status changes.
- Modified the frontend to include a checkbox for `NotifyOnStatusChange` in the node editing dialog, allowing admins to easily configure this setting.
- Updated API documentation to reflect the new `notifyOnStatusChange` parameter in the node update endpoint.
- Added tests to ensure the correct behavior of the new feature and its integration with existing functionality.
This commit is contained in:
Leonid Pershin
2026-07-23 11:04:38 +03:00
parent 2f6bb26e97
commit fb97093bd6
20 changed files with 1272 additions and 6 deletions
@@ -55,6 +55,7 @@ public static class NodeEndpoints
body.BaseAddress,
body.Location,
body.IsEnabled,
body.NotifyOnStatusChange,
body.Username,
body.Password
);
@@ -98,6 +99,7 @@ public sealed record UpdateNodeBody(
string BaseAddress,
string? Location,
bool IsEnabled,
bool NotifyOnStatusChange,
string? Username,
string? Password
);
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Options;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Domain.Billing;
using PnvPanel.Domain.Nodes;
using PnvPanel.Domain.Support;
using PnvPanel.Infrastructure.Telegram;
using Telegram.Bot;
@@ -358,6 +359,48 @@ internal sealed class TelegramNotifier(
}
}
public async Task NotifyAdminsNodeStatusChangedAsync(
Guid nodeId,
string nodeName,
NodeStatus status,
CancellationToken cancellationToken
)
{
if (string.IsNullOrWhiteSpace(options.Value.BotToken))
return;
var emoji = status == NodeStatus.Online ? "✅" : "🔴";
var statusLabel = status == NodeStatus.Online ? "снова в сети" : "недоступна";
var text = $"{emoji} Нода <b>{Escape(nodeName)}</b> {statusLabel}.";
InlineKeyboardMarkup? keyboard = null;
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
{
var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/nodes";
keyboard = new InlineKeyboardMarkup(
new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) }
);
}
foreach (var adminId in options.Value.ParseAdminTelegramUserIds())
{
try
{
await botClient.SendMessage(
adminId,
text,
parseMode: ParseMode.Html,
replyMarkup: keyboard,
cancellationToken: cancellationToken
);
}
catch
{
// Админ мог не запускать бота (нет чата с ботом) — пропускаем, не валим команду.
}
}
}
private static string Escape(string text) =>
text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
}