- 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.
14 KiB
Frontend
SPA на React 19 + Vite + TypeScript. Общается с бэком по REST (JWT Bearer) и получает живые обновления по SignalR.
Раздача из единого контейнера. В проде собранный фронт (
dist/) кладётся вwwwrootASP.NET Core и раздаётся тем же приложением (SPA-fallback наindex.html). Фронт и бек — один origin, база API — относительный/api, SignalR —/hubs/panel. В dev Vite-сервер проксирует/apiи/hubsна бэкенд (vite.config.ts, цель —http://localhost:8080по умолчанию, переопределяетсяVITE_API_TARGET). Детали упаковки — architecture.md.
Стек
| Задача | Выбор |
|---|---|
| Сборка/dev | Vite (Rolldown-based) |
| Язык | TypeScript (strict) |
| Данные с сервера | TanStack Query |
| Роутинг | TanStack Router (файловый, src/routes/, кодогенерация routeTree.gen.ts) |
| UI-компоненты | shadcn-стиль поверх Radix (@radix-ui/react-*) + Tailwind CSS v4 |
| Иконки | lucide-react |
| Клиентский стейт | Zustand — только auth-стор (features/auth/store.ts); тема — React Context + localStorage, не Zustand |
| Формы | react-hook-form + zod |
| Realtime | @microsoft/signalr |
| QR-коды | qrcode.react (рендерит QR из готовой строки на клиенте) |
| Markdown | react-markdown + remark-gfm (новости, инструкции; без rehype-raw — сырой HTML не рендерится) |
| Типы API | openapi-typescript (pnpm gen:api) — генерирует schema.gen.ts для сверки; фичи импортируют руками написанный shared/api/types.ts |
| i18n | react-i18next (RU + EN) |
| Линт | oxlint (не ESLint) |
| Пакетный менеджер | pnpm |
Установлены, но не используются: recharts (админская статистика — карточки с цифрами, без
графиков), @tanstack/react-table (админские таблицы написаны руками, без TanStack Table).
Структура
Фактическая структура (frontend/src/):
frontend/
src/
main.tsx # точка входа: QueryClientProvider > ThemeProvider > ToastProvider > RealtimeProvider > RouterProvider
router.tsx # createRouter из routeTree.gen.ts
routeTree.gen.ts # сгенерировано @tanstack/router-plugin, не редактируется руками
index.css # Tailwind v4 (@import), без отдельной styles/-папки
routes/ # файловый роутинг TanStack Router
__root.tsx # шапка (лого, нав, переключатель языка/темы), Outlet
index.tsx, login.tsx, register.tsx, dashboard.tsx, instructions.tsx, news.tsx, settings.tsx
admin.tsx # layout админки (вкладки) + Outlet
admin/
index.tsx, activation.tsx, users.tsx, roles.tsx, nodes.tsx, apps.tsx, news.tsx, audit.tsx
features/
auth/ # api.ts, store.ts (zustand), guards.ts, LoginForm.tsx, RegisterForm.tsx
activation/ # api.ts, ActivationGate.tsx (экран "запросить активацию")
configs/ # api.ts, ConfigCard.tsx, CreateConfigDialog.tsx, SubscriptionCard.tsx
apps/ # api.ts, AppsCatalog.tsx (для /instructions)
news/ # api.ts, NewsFeed.tsx (для /news)
telegram/ # api.ts, TelegramLoginButton.tsx
settings/ # ChangePasswordForm.tsx, TelegramLinkCard.tsx, DeleteAccountSection.tsx
admin/
users/, roles/, activation/, nodes/, inbounds/, apps/, news/, audit/, stats/ # api.ts + диалоги CRUD в каждой
theme/
ThemeProvider.tsx # React Context + localStorage (`pnv-theme`), НЕ zustand
shared/
api/
client.ts # apiRequest(), HttpError, access-token в памяти модуля, 401 → silent refresh → повтор
types.ts # руками написанные типы ответов/запросов (сверены со schema.gen.ts)
schema.gen.ts # генерируется pnpm gen:api, не импортируется напрямую фичами
lib/
i18n.ts, cn.ts, format.ts
realtime/
connection.ts, RealtimeProvider.tsx
ui/
button.tsx, input.tsx, textarea.tsx, label.tsx, card.tsx, dialog.tsx, select.tsx, badge.tsx,
progress.tsx, toast-store.tsx, toaster.tsx
index.html
vite.config.ts
package.json
Нет app/-папки с провайдерами (они прямо в main.tsx), нет shared/config/ (переменные окружения
читаются точечно через import.meta.env), нет styles/ (один index.css с Tailwind).
Дизайн / UX
- Тема оформления: светлая/тёмная/системная (переключатель в шапке). Реализация — класс
darkна<html>+ Tailwind, выбор вlocalStorage(ThemeProvider, React Context). - Дашборд пользователя (
/dashboard): карточки конфигов (протокол, локация, использованный трафик, статус), кнопки на карточке — показать ссылку/QR (запрашиваетGET .../linkпо клику, не сразу при создании), перевыпустить, отозвать; отдельная карточка «Общая подписка». Для неактивированного —ActivationGateвместо дашборда. - Создание конфига: диалог — выбор инбаунда (по
displayName) + метка. Лимит одновременных IP (limitIpв 3x-ui) выставляется автоматически по квоте роли пользователя (AppRole.MaxIpLimit) — в форме создания не настраивается. После успеха карточка конфига появляется в списке; ссылку/QR пользователь открывает отдельно. - Страница инструкций (
/instructions): статичные шаги + каталог приложений (GET /api/apps), сгруппированный по ОС и показан вкладками (по одной ОС за раз); клик по приложению открывает ссылку на скачивание. - Лента новостей (
/news): хронологический список постов админа (заголовок + Markdown-тело, рендерится черезreact-markdown+remark-gfm), пагинация (GET /api/news), живое обновление по SignalR (newsPublished, широковещательно всем). Админка (/admin/news): CRUD, обычный<textarea>с переключателем предпросмотра Markdown вместо WYSIWYG-редактора. - Markdown-редактор админки (
features/admin/media/MarkdownEditor.tsx) — общий для новостей и инструкций (интро + вкладки): текст, предпросмотр и загрузка картинок (кнопка, вставка из буфера, drag&drop). Файл уходит вPOST /api/admin/media/images, а в текст на позицию курсора вставляется; картинка отдаётся анонимно, поэтому подмена компонентаimgвreact-markdownне нужна. Оформление отрендеренного markdown — общийMARKDOWN_CLASSES(shared/lib/markdown.ts), он же ограничивает картинки по ширине. - Настройки (
/settings): смена пароля, привязка/отвязка Telegram (TelegramLinkCard), удаление аккаунта с подтверждением (DeleteAccountSection). - Админка (
/admin/*): вкладки — обзор (карточки статистики, без графиков), запросы активации, пользователи, роли, ноды (+ публикация инбаундов), приложения, аудит. Таблицы — обычные<table>, без TanStack Table. Блокировка пользователя — с подтверждением. - Пользователи (
/admin/users): колонка «Конфигов» показывает квоту (configQuota,∞для безлимита) и рядом имя тарифа либо «своя квота». ВUserManageDialog— блок квоты: выбор каталожного тарифа (применяется сразу, как смена роли) либо своё число конфигов черезPATCH /api/admin/users/{id}/plan. Подпись явно говорит, что админская смена идёт без доплаты и что при понижении уже созданные конфиги не отзываются. - Состояния:
isLoading/isError/пусто различаются явно везде (ошибка сети не выглядит как «пусто» — паттерн закреплён после находки вActivationGate, распространён на все admin-списки).
Работа с API
shared/api/client.ts:apiRequest<T>()оборачиваетfetch, подставляет access-token вAuthorization; на401— один прозрачныйPOST /api/auth/refreshи повтор запроса; при неуспехе рефреша —setUnauthorizedHandlerколбэк (разлогин).- Запросы/мутации — через TanStack Query, ключи по фиче, инвалидация после мутаций.
- Типы — из
shared/api/types.ts(см. таблицу стека выше и tech-stack.md).
Авторизация на клиенте
- Access-token — в памяти (модуль
client.ts, не React state и не localStorage) — переживает ре-рендеры, но не пережить reload (тогда его молча восстанавливает silent refresh по cookie). - Refresh-token — httpOnly Secure cookie (
pnv_refresh_token,Path=/api/auth), JS его не видит. - Стор авторизации (
features/auth/store.ts, Zustand) хранит текущего пользователя/isActivated. Guard-хукиuseRequireAuth()/useRequireGuest()/useRequireAdmin()(features/auth/guards.ts) редиректят черезuseNavigate()вuseEffect, если условие не выполнено. - Вход — по username (email не используется). «Забыли пароль?»: при привязанном Telegram — вход через бота и смена пароля в настройках; иначе — обратиться к админу.
Вход и привязка через Telegram
- «Войти через Telegram» (
TelegramLoginButton, на/login):POST .../login-request→ показываетdeepLinkкак QR (qrcode.react), поллитGET .../login-request/{id}доApproved/Rejected/Expired. ПриApproved— сохраняетaccessTokenв память клиента (refresh уже пришёл в cookie от бэка) и редиректит в панель. - «Привязать Telegram» (
TelegramLinkCard, в настройках):POST .../link-token→ QR изdeepLink; статус обновляется поллингом. Отвязка —POST .../unlink.
Realtime
- Одно SignalR-подключение к
/hubs/panelс JWT (RealtimeProvider,shared/realtime/connection.ts). - Обработчики
configTrafficUpdated/configStatusChanged/nodeStatusChanged/activationRequested/userActivated/newsPublishedточечно инвалидируют/обновляют кэш TanStack Query — UI обновляется без перезагрузки.
Скрипты
pnpm dev # dev-сервер Vite
pnpm build # tsc -b && vite build (прод-сборка)
pnpm preview # предпросмотр prod-сборки
pnpm lint # oxlint
pnpm typecheck # tsc -b
pnpm gen:api # openapi-typescript по /openapi/v1.json живого бэкенда -> schema.gen.ts
pnpm gen:api требует запущенный бэкенд на http://localhost:8080 (или docker compose up).