Files
PnvPanel/docs/frontend.md
T
Leonid Pershin ad94c6ef22
CI / Backend (build + test) (push) Successful in 1m33s
CI / Frontend (lint + typecheck + build) (push) Successful in 29s
Update documentation and clarify MVP status
- Revised the CLAUDE.md and README.md files to reflect the current MVP status, emphasizing completed features and intentionally omitted elements such as traffic limits and billing.
- Enhanced clarity in the documentation regarding the architecture, tech stack, and user roles.
- Removed the outdated roadmap section and streamlined references to tech stack decisions.
- Updated API design documentation to clarify the absence of versioning in the MVP and the handling of configuration details.
2026-07-02 21:11:59 +03:00

147 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` на
`<html>` + Tailwind, выбор в `localStorage` (`ThemeProvider`, React Context).
- **Дашборд пользователя** (`/dashboard`): карточки конфигов (протокол, локация, использованный
трафик, статус), кнопки на карточке — показать ссылку/QR (запрашивает `GET .../link` по клику,
не сразу при создании), перевыпустить, отозвать; отдельная карточка «Общая подписка». Для
неактивированного — `ActivationGate` вместо дашборда.
- **Создание конфига**: диалог — выбор инбаунда (по `displayName`) + метка + лимит устройств.
После успеха карточка конфига появляется в списке; ссылку/QR пользователь открывает отдельно.
- **Страница инструкций** (`/instructions`): статичные шаги + каталог приложений (`GET /api/apps`),
сгруппированный по ОС; клик по приложению открывает ссылку на скачивание.
- **Настройки** (`/settings`): смена пароля, привязка/отвязка Telegram (`TelegramLinkCard`),
удаление аккаунта с подтверждением (`DeleteAccountSection`).
- **Админка** (`/admin/*`): вкладки — обзор (карточки статистики, без графиков), запросы активации,
пользователи, роли, ноды (+ публикация инбаундов), приложения, аудит. Таблицы — обычные `<table>`,
без TanStack Table. Блокировка пользователя — с подтверждением.
- **Состояния**: `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](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`).