# 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](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 из готовой строки на клиенте) | | Типы 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, settings.tsx admin.tsx # layout админки (вкладки) + Outlet admin/ index.tsx, activation.tsx, users.tsx, roles.tsx, nodes.tsx, apps.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) telegram/ # api.ts, TelegramLoginButton.tsx settings/ # ChangePasswordForm.tsx, TelegramLinkCard.tsx, DeleteAccountSection.tsx admin/ users/, roles/, activation/, nodes/, inbounds/, apps/, 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, 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` на `` + Tailwind, выбор в `localStorage` (`ThemeProvider`, React Context). - **Дашборд пользователя** (`/dashboard`): карточки конфигов (протокол, локация, использованный трафик, статус), кнопки на карточке — показать ссылку/QR (запрашивает `GET .../link` по клику, не сразу при создании), перевыпустить, отозвать; отдельная карточка «Общая подписка». Для неактивированного — `ActivationGate` вместо дашборда. - **Создание конфига**: диалог — выбор инбаунда (по `displayName`) + метка. Лимит устройств (`limitIp` в 3x-ui) панелью не управляется — задаётся при необходимости напрямую в 3x-ui. После успеха карточка конфига появляется в списке; ссылку/QR пользователь открывает отдельно. - **Страница инструкций** (`/instructions`): статичные шаги + каталог приложений (`GET /api/apps`), сгруппированный по ОС и показан вкладками (по одной ОС за раз); клик по приложению открывает ссылку на скачивание. - **Настройки** (`/settings`): смена пароля, привязка/отвязка Telegram (`TelegramLinkCard`), удаление аккаунта с подтверждением (`DeleteAccountSection`). - **Админка** (`/admin/*`): вкладки — обзор (карточки статистики, без графиков), запросы активации, пользователи, роли, ноды (+ публикация инбаундов), приложения, аудит. Таблицы — обычные ``, без TanStack Table. Блокировка пользователя — с подтверждением. - **Состояния**: `isLoading`/`isError`/пусто различаются явно везде (ошибка сети не выглядит как «пусто» — паттерн закреплён после находки в `ActivationGate`, распространён на все admin-списки). ## Работа с API - `shared/api/client.ts`: `apiRequest()` оборачивает `fetch`, подставляет access-token в `Authorization`; на `401` — один прозрачный `POST /api/auth/refresh` и повтор запроса; при неуспехе рефреша — `setUnauthorizedHandler` колбэк (разлогин). - Запросы/мутации — через TanStack Query, ключи по фиче, инвалидация после мутаций. - Типы — из `shared/api/types.ts` (см. таблицу стека выше и [tech-stack.md](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` точечно инвалидируют/обновляют кэш TanStack Query — UI обновляется без перезагрузки. ## Скрипты ```bash 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`).