Enhance user management and node health check features
CI / Backend (build + test) (push) Failing after 2m14s
CI / Frontend (lint + typecheck + build) (push) Successful in 51s

- Updated `ListUsersQueryHandler` to include plan names and config quotas in `UserSummaryDto`, enriching user data retrieval.
- Implemented `WithPlanNamesAsync` method to fetch plan names based on user plan IDs, improving user experience in the admin interface.
- Enhanced `Node` class with a `ConsecutiveProbeFailures` property for better status management during health checks.
- Modified `NodeHealthCheckService` to utilize the new `RecordProbe` method, implementing a hysteresis mechanism for node status changes.
- Updated frontend components to display user config quotas and plan names, improving clarity in user management.
- Enhanced tests for user listing and node status handling to ensure robust functionality and coverage.
- Updated documentation to reflect changes in user and node management features.
This commit is contained in:
Leonid Pershin
2026-08-05 08:34:17 +03:00
parent c2ed3240bd
commit 4b34c37ce3
20 changed files with 1563 additions and 28 deletions
+7
View File
@@ -382,6 +382,13 @@ approve/reject над `ActivationRequest`.
отличие от `VpnConfigDto`): `{ id, userId, userName, label, clientEmail, protocol, location, nodeName,
usedUpBytes, usedDownBytes, expiresAt, status, createdAt }`. `search` матчится по `clientEmail`/`label`.
`UserSummaryDto`: `{ id, userName, role, isActivated, isBlocked, activatedAt, billingEnabled,
billingPaidUntil, configQuota, planId, billingPendingReview, planName }`. `configQuota` — фактическая
квота конфигов (`-1` = без лимита), `planName` — имя каталожного тарифа по `planId` (`null`, если
квота задана вручную); и `planName`, и `billingPendingReview` домешивает `ListUsersQueryHandler`
`IIdentityService` не знает ни про `Plans`, ни про `PaymentRequests`. Менять квоту админ может через
`PATCH /api/admin/users/{id}/plan` (см. Admin — Roles & Plans), доплата при этом не создаётся.
**Блокировка/разблокировка — два отдельных эндпоинта без тела**, не один переключатель `isBlocked`.
`StatsDto`: `{ totalUsers, activatedUsers, pendingActivationRequests, totalNodes, onlineNodes,
totalConfigs, activeConfigs, totalUsedUpBytes, totalUsedDownBytes }` — считается на лету при запросе,
+8 -3
View File
@@ -227,9 +227,14 @@ POST /api/configs
клиентам через `IXuiPanelGateway.GetClientTrafficAsync`, пишет `VpnConfig.UpdateTraffic(...)` и
`TrafficSample`, шлёт `configTrafficUpdated`. Трафик используется только для отображения — лимиты
и автоотключение по превышению не реализованы (см. [domain-model.md](domain-model.md)).
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
шлёт `nodeStatusChanged` группе `admins`. Дополнительно, если у ноды `NotifyOnStatusChange = true`,
при каждом переходе Online↔Offline шлёт админам ещё и Telegram-уведомление
- **NodeHealthCheckService** — health-probe нод раз в 2 минуты (`IXuiPanelGateway.ProbeAsync`), обновляет
`NodeStatus` через `Node.RecordProbe` с **гистерезисом**: Offline выставляется только после 2 подряд
неудачных проб (счётчик `Node.ConsecutiveProbeFailures`, обнуляется первой удачной пробой), обратно в
Online — по первой же удачной. Так единичные HTTP-заминки панели (таймаут/реавторизация/`No route to
host` при моргании сети, ICMP при этом в норме) не порождают ложных переходов и спама уведомлениями.
При смене статуса шлёт `nodeStatusChanged` группе `admins`; причина падения и счётчик неудач
логируются. Дополнительно, если у ноды `NotifyOnStatusChange = true`, при каждом переходе
Online↔Offline шлёт админам ещё и Telegram-уведомление
(`ITelegramNotifier.NotifyAdminsNodeStatusChangedAsync`) — опция включается индивидуально на ноду
(`PUT /api/admin/nodes/{id}`), по умолчанию выключена.
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
+4 -2
View File
@@ -50,6 +50,7 @@ AppUser
| `Credentials` | `NodeCredentials` (VO) | Логин + **зашифрованный** пароль (`ISecretProtector`) |
| `Location` | `string?` | Страна/город/тег для выбора пользователем |
| `Status` | `NodeStatus` | `Online` / `Offline` / `Unknown` |
| `ConsecutiveProbeFailures` | `int` | Счётчик подряд неудачных проб для гистерезиса статуса (`RecordProbe`); обнуляется первой удачной пробой |
| `IsEnabled` | `bool` | Выключена админом → скрыта из самообслуживания |
| `NotifyOnStatusChange` | `bool` | Слать админам в Telegram при каждом переходе Online↔Offline (см. NodeHealthCheckService); по умолчанию `false` |
| `LastSyncAt` | `DateTimeOffset?` | Последняя успешная синхронизация |
@@ -111,8 +112,9 @@ AppUser
не показывают вовсе, а админский список подставляет "?" вместо локации отсутствующего инбаунда.
> `Node.Status` (health-check раз в 2 минуты, см. `NodeHealthCheckService`) — это диагностический
> индикатор для админа, не гейт для создания конфига: он кэшированный и может ложно показывать
> `Offline` из-за временного сбоя пробника. Реальную недоступность ноды ловит вызов
> индикатор для админа, не гейт для создания конфига: он кэшированный и, несмотря на гистерезис
> (`RecordProbe`: Offline лишь после 2 подряд неудачных проб), может отставать от реальности. Реальную
> недоступность ноды ловит вызов
> `IXuiPanelGateway.AddClientAsync` в момент создания — с честной ошибкой и компенсацией
> зарезервированной квоты, а не заранее закэшированным статусом.
+5
View File
@@ -111,6 +111,11 @@ frontend/
- **Админка** (`/admin/*`): вкладки — обзор (карточки статистики, без графиков), запросы активации,
пользователи, роли, ноды (+ публикация инбаундов), приложения, аудит. Таблицы — обычные `<table>`,
без TanStack Table. Блокировка пользователя — с подтверждением.
- **Пользователи** (`/admin/users`): колонка «Конфигов» показывает квоту (`configQuota`, `∞` для
безлимита) и рядом имя тарифа либо «своя квота». В `UserManageDialog` — блок квоты: выбор
каталожного тарифа (применяется сразу, как смена роли) либо своё число конфигов через
`PATCH /api/admin/users/{id}/plan`. Подпись явно говорит, что админская смена идёт **без доплаты**
и что при понижении уже созданные конфиги не отзываются.
- **Состояния**: `isLoading`/`isError`/пусто различаются явно везде (ошибка сети не выглядит как
«пусто» — паттерн закреплён после находки в `ActivationGate`, распространён на все admin-списки).