Add news feature with CRUD operations and real-time notifications
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s

- 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.
This commit is contained in:
Leonid Pershin
2026-07-03 15:28:33 +03:00
parent bea2b5fcf7
commit b6637a1c03
47 changed files with 2523 additions and 9 deletions
+15
View File
@@ -101,6 +101,20 @@ status, createdAt }`. `expiresAt` всегда `null` (лимиты по сро
```
Значение `OsPlatform` в C#/JSON`IOS` (не `iOS`).
## News — лента новостей
| Метод | Путь | Роль | Тело запроса | Тело ответа |
| ------ | ------------------------ | ----- | ------------------------ | ------------- |
| GET | `/api/news` | user | query: `page, pageSize` | `PagedList<NewsPostDto>` |
| GET | `/api/admin/news` | admin | query: `page, pageSize` | `PagedList<NewsPostDto>` |
| POST | `/api/admin/news` | admin | `{ title, body }` | `NewsPostDto` |
| PUT | `/api/admin/news/{id}` | admin | `{ title, body }` | `NewsPostDto` |
| DELETE | `/api/admin/news/{id}` | admin | — | `204 No Content` |
Нет черновиков/отложенной публикации — `POST` сразу видна всем аутентифицированным пользователям
и триггерит SignalR-событие `newsPublished` (см. ниже). `NewsPostDto`:
`{ id, title, body, createdAt, updatedAt }`.
## Activation (пользователь)
| Метод | Путь | Роль | Тело запроса | Тело ответа |
@@ -190,6 +204,7 @@ totalConfigs, activeConfigs, totalUsedUpBytes, totalUsedDownBytes }` — счи
| `nodeStatusChanged` | `{ nodeId, status, lastSyncAt }` | `admins` |
| `activationRequested` | `{ requestId, userId, userName, comment, createdAt }` | `admins` |
| `userActivated` | `{ userId }` | владельцу |
| `newsPublished` | `{ id, title, createdAt }` | все (broadcast) |
### Client → Server
Клиент только слушает; группировка по пользователю происходит на сервере при подключении, по
+3 -1
View File
@@ -189,7 +189,9 @@ POST /api/configs
(события нод/системы/активации) — пользователь при подключении добавляется в свою `user:{userId}`
и, если он админ, дополнительно в `admins`.
- **События сервер→клиент**: `configTrafficUpdated`, `configStatusChanged`, `nodeStatusChanged`,
`activationRequested`, `userActivated` — точные payload'ы см. [api-design.md](api-design.md#signalr--hub-hubspanel).
`activationRequested`, `userActivated`, `newsPublished` — точные payload'ы см.
[api-design.md](api-design.md#signalr--hub-hubspanel). `newsPublished` — единственное
широковещательное событие (`Clients.All`), а не по группе — новости видны всем без исключения.
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`, реализация в `Api/Hubs/`),
вызываемый из хендлеров и фоновых сервисов — Application-слой не зависит от SignalR напрямую.
+4 -1
View File
@@ -12,6 +12,7 @@ backend/
Common/ # Entity (единственный базовый класс — без AggregateRoot/IDomainEvent)
Activation/ # ActivationRequest, ActivationStatus
Apps/ # ClientApp, OsPlatform
News/ # NewsPost
Audit/ # AuditLog, AuditSource
Configs/ # VpnConfig, ConfigStatus, TrafficSample
Inbounds/ # Inbound, VpnProtocol
@@ -29,6 +30,7 @@ backend/
Admin/
Activation/ # ListActivationRequestsQuery, Approve/RejectActivationCommand
Apps/ # CRUD ClientApp
News/ # CRUD NewsPost
Audit/ # ListAuditLogsQuery
Inbounds/ # ListInbounds, PublishInbound
Nodes/ # RegisterNode, UpdateNode, DeleteNode, SyncNode, ProbeNode, ListNodes
@@ -37,6 +39,7 @@ backend/
Users/ # ListUsers, BlockUser/UnblockUser, ChangeUserRole, ResetUserPassword,
# ForceRevokeConfig, GetUserConfigs
Apps/ # ListAppsQuery (по ОС, для юзера)
News/ # ListNewsQuery (пагинировано, для юзера)
Auth/
ChangePassword/, DeleteMyAccount/, Login/, Logout/, Me/, Refresh/, Register/
Configs/
@@ -59,7 +62,7 @@ backend/
Telegram/ # TelegramNotifier, TelegramOptions
DependencyInjection.cs # AddInfrastructure(...)
PnvPanel.Api/
Endpoints/ # 12 файлов, см. backend-conventions.md ниже и api-design.md
Endpoints/ # 14 файлов, см. backend-conventions.md ниже и api-design.md
Hubs/ # PanelHub, SignalRRealtimeNotifier (реализация IRealtimeNotifier — здесь,
# не в Infrastructure, т.к. нужен IHubContext<PanelHub>)
Telegram/ # TelegramBotHostedService, PnvBotUpdateHandler, TelegramNotifier
+16
View File
@@ -21,6 +21,7 @@ AppUser (Identity) [+ IsActivated, IsBlocked, TelegramUserId, SubscriptionToken
VpnConfig ─*─ TrafficSample (история трафика; пишется TrafficSyncService)
AuditLog (append-only журнал действий; ссылается на ActorId/TargetId)
ClientApp (каталог приложений-клиентов; группируется по OperatingSystem)
NewsPost (лента новостей; публикуется админом, видна всем аутентифицированным пользователям)
```
## Сущности
@@ -143,6 +144,21 @@ ClientApp (каталог приложений-клиен
Управляется админом (CRUD). Пользователю отдаётся только `IsEnabled`, сгруппировано по `OperatingSystem`.
Стартовый набор сидируется из [`seed/client-apps.json`](../seed/client-apps.json), если таблица пуста.
### NewsPost — новости для пользователей
Публикуются админом немедленно, видны всем залогиненным пользователям в хронологической ленте.
| Поле | Тип | Заметки |
| ----------- | ----------------- | ------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `Title` | `string` | Заголовок |
| `Body` | `string` | Markdown-текст |
| `CreatedAt` | `DateTimeOffset` | Момент публикации (= момент создания, нет черновиков) |
| `UpdatedAt` | `DateTimeOffset?` | Момент последней правки (опц.) |
Нет статуса черновик/запланировано — публикация мгновенная. Нет видимости по ролям — доступно
всем аутентифицированным пользователям. Realtime-оповещение о новом посте — `newsPublished`
(SignalR, широковещательно всем подключенным клиентам), см. [architecture.md](architecture.md#realtime-signalr).
### AuditLog — журнал действий
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.
+12 -5
View File
@@ -23,6 +23,7 @@ SPA на **React 19 + Vite + TypeScript**. Общается с бэком по R
| Формы | 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) |
@@ -44,19 +45,20 @@ frontend/
index.css # Tailwind v4 (@import), без отдельной styles/-папки
routes/ # файловый роутинг TanStack Router
__root.tsx # шапка (лого, нав, переключатель языка/темы), Outlet
index.tsx, login.tsx, register.tsx, dashboard.tsx, instructions.tsx, settings.tsx
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, audit.tsx
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/, audit/, stats/ # api.ts + диалоги CRUD в каждой
users/, roles/, activation/, nodes/, inbounds/, apps/, news/, audit/, stats/ # api.ts + диалоги CRUD в каждой
theme/
ThemeProvider.tsx # React Context + localStorage (`pnv-theme`), НЕ zustand
shared/
@@ -69,7 +71,7 @@ frontend/
realtime/
connection.ts, RealtimeProvider.tsx
ui/
button.tsx, input.tsx, label.tsx, card.tsx, dialog.tsx, select.tsx, badge.tsx,
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
@@ -93,6 +95,10 @@ frontend/
- **Страница инструкций** (`/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/*`): вкладки — обзор (карточки статистики, без графиков), запросы активации,
@@ -132,7 +138,8 @@ frontend/
- Одно SignalR-подключение к `/hubs/panel` с JWT (`RealtimeProvider`, `shared/realtime/connection.ts`).
- Обработчики `configTrafficUpdated`/`configStatusChanged`/`nodeStatusChanged`/`activationRequested`/
`userActivated` точечно инвалидируют/обновляют кэш TanStack Query — UI обновляется без перезагрузки.
`userActivated`/`newsPublished` точечно инвалидируют/обновляют кэш TanStack Query — UI обновляется
без перезагрузки.
## Скрипты
+1
View File
@@ -110,6 +110,7 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
- Telegram-бот: ссылка на сайт, просмотр конфигов, привязка/регистрация через Telegram и passwordless-вход.
- Светлая/тёмная тема сайта.
- Страница инструкций по подключению + каталог приложений по ОС (админ ведёт, юзер видит сгруппировано).
- Лента новостей: админ публикует Markdown-посты, все пользователи видят их живой лентой (SignalR).
- Единый Docker-образ (фронт+бек) + PostgreSQL в docker-compose.
**Не реализовано:**