Files
PnvPanel/docs/frontend.md
T
Leonid Pershin b6637a1c03
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s
Add news feature with CRUD operations and real-time notifications
- Implemented news management functionality, allowing admins to create, read, update, and delete news posts.
- Introduced a new SignalR event for broadcasting news updates to all connected clients.
- Updated API documentation to include new endpoints for news management.
- Enhanced frontend with a dedicated news page and admin interface for managing news posts.
- Added necessary localization for news-related terms in both Russian and English.
2026-07-03 15:28:33 +03:00

156 lines
12 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 из готовой строки на клиенте) |
| 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`) + метка. Лимит устройств
(`limitIp` в 3x-ui) панелью не управляется — задаётся при необходимости напрямую в 3x-ui. После
успеха карточка конфига появляется в списке; ссылку/QR пользователь открывает отдельно.
- **Страница инструкций** (`/instructions`): статичные шаги + каталог приложений (`GET /api/apps`),
сгруппированный по ОС и показан вкладками (по одной ОС за раз); клик по приложению открывает
ссылку на скачивание.
- **Лента новостей** (`/news`): хронологический список постов админа (заголовок + Markdown-тело,
рендерится через `react-markdown` + `remark-gfm`), пагинация (`GET /api/news`), живое обновление
по SignalR (`newsPublished`, широковещательно всем). Админка (`/admin/news`): CRUD, обычный
`<textarea>` с переключателем предпросмотра Markdown вместо WYSIWYG-редактора.
- **Настройки** (`/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`/`newsPublished` точечно инвалидируют/обновляют кэш 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`).