Files
PnvPanel/docs/frontend.md
T
Leonid Pershin 4b34c37ce3
CI / Backend (build + test) (push) Failing after 2m14s
CI / Frontend (lint + typecheck + build) (push) Successful in 51s
Enhance user management and node health check features
- 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.
2026-08-05 08:34:17 +03:00

14 KiB
Raw Blame History

Frontend

SPA на React 19 + Vite + TypeScript. Общается с бэком по REST (JWT Bearer) и получает живые обновления по SignalR.

Раздача из единого контейнера. В проде собранный фронт (dist/) кладётся в wwwroot ASP.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, а в текст на позицию курсора вставляется ![имя](/api/media/images/{id}); картинка отдаётся анонимно, поэтому подмена компонента 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).