07-04-2026

Сделал вкладку параметры пользователя РАБОТАЮЩЕЙ

Добавил
- Локальный запуск
- техническое задание 1 (доработать ui что бы RFYFKS работали)
This commit is contained in:
AidarKC 2026-04-07 14:43:08 +03:00
parent 0b7ad79032
commit 0c7d8fac02
6 changed files with 247 additions and 234 deletions

View File

@ -0,0 +1,220 @@
# Задача 01: Доработка вкладки «Каналы» (UI + API)
## Кратко и по делу
Нужно довести вторую вкладку «Каналы» до полностью рабочего состояния на реальных данных сервера.
Что должно работать:
- список каналов;
- вход в канал и чтение сообщений;
- вход в тред сообщения (история/ветка);
- ответ на сообщение;
- лайк/снятие лайка;
- подписка на пользователя;
- подписка на канал;
- видимое имя канала в формате `имя_пользователя/имя_канала`.
Запись любых новых сущностей делается через `AddBlock` с подписью на клиенте.
Чтение делается через 3 API:
- `ListSubscriptionsFeed`
- `GetChannelMessages`
- `GetMessageThread`
Техническая особенность (оставляем как есть):
- на экране каналов индикатор непрочитанного = общее число сообщений канала.
---
## Подробное ТЗ
### 1. Цель
Сделать рабочий каналовый сценарий «от списка до треда», где чтение строится на RPC API, а запись действий пользователя — только через `AddBlock`.
### 2. Что уже есть в проекте
#### 2.1 UI (частично)
- Есть страницы:
- `channels-list`
- `channel-view`
- `add-channel-view`
- Есть запросы чтения в клиенте:
- `authService.listSubscriptionsFeed(...)`
- `authService.getChannelMessages(...)`
- `authService.getMessageThread(...)`
- Есть fallback на mock-данные при ошибках сервера.
#### 2.2 API/сервер (уже реализованы)
- `ListSubscriptionsFeed`
- `GetChannelMessages`
- `GetMessageThread`
- `AddBlock`
#### 2.3 Тесты
- Есть интеграционный тест API каналов: `IT_06_ChannelsApi`.
- Есть тесты генерации блоков каналов/связей: `IT_03_AddBlock_NoAuth`.
- Формат `AddBlock` и его сборка/подпись описаны в `AddBlockSender`.
### 3. Проблемы текущей реализации (что надо закрыть)
- Кнопки «подписаться на человека/канал» в списке каналов сейчас UI-only (модалка без реальной записи через `AddBlock`).
- `add-channel-view` пока не создает канал на сервере через `AddBlock` (`CreateChannelBody`), только делает `navigate`.
- `channel-view` добавляет пост локально (в память), а не отправляет блок `TEXT_POST` через `AddBlock`.
- Нет полноценного экрана треда сообщения с реальными `GetMessageThread` и действиями `ответить/лайк/убрать лайк` через блоки.
- Нет гарантированного отображения канала в требуемом формате `ownerLogin/channelName`.
### 4. Функциональные требования
#### 4.1 Список каналов
На вкладке «Каналы» отображать 3 группы:
- Мои каналы
- Каналы пользователей, на кого я подписан
- Каналы, на которые я подписан
Источник данных: `ListSubscriptionsFeed`.
Каждый канал показывать в формате:
- `ownerLogin/channelName`
#### 4.2 Открытие канала
При входе в канал:
- загрузить сообщения через `GetChannelMessages`;
- показать список сообщений в хронологическом порядке (по текущему параметру `sort`);
- оставить техническую особенность непрочитанных как есть.
#### 4.3 Открытие треда сообщения
При клике на сообщение:
- загрузить тред через `GetMessageThread`;
- показать `ancestors`, `focus`, `descendants`;
- из треда должны быть доступны действия:
- «Ответить»
- «Лайк»
- «Убрать лайк»
Запись действий — только `AddBlock`.
#### 4.4 Создание канала
В `add-channel-view` кнопка «Создать» должна:
- отправлять `AddBlock` с телом `CreateChannelBody`;
- после успеха возвращать к списку каналов и обновлять его.
#### 4.5 Подписки
- Подписка на пользователя: `AddBlock` с `ConnectionBody` подтип `CONNECTION_FOLLOW`, target = HEADER пользователя.
- Подписка на канал: `AddBlock` с `ConnectionBody` подтип `CONNECTION_FOLLOW`, target = root блока канала (`CreateChannelBody` или HEADER для канала `0`).
### 5. API (форматы)
## 5.1 ListSubscriptionsFeed (чтение)
Request:
```json
{
"op": "ListSubscriptionsFeed",
"requestId": "...",
"payload": {
"login": "A1",
"limit": 200
}
}
```
Response (смысловые поля):
- `ownedChannels[]`
- `followedUsersChannels[]`
- `followedChannels[]`
---
## 5.2 GetChannelMessages (чтение)
Request:
```json
{
"op": "GetChannelMessages",
"requestId": "...",
"payload": {
"channel": {
"ownerBlockchainName": "A1-001",
"channelRootBlockNumber": 0,
"channelRootBlockHash": ""
},
"limit": 200,
"sort": "asc"
}
}
```
Response (смысловые поля):
- `channel`
- `messages[]`
---
## 5.3 GetMessageThread (чтение)
Request:
```json
{
"op": "GetMessageThread",
"requestId": "...",
"payload": {
"message": {
"blockchainName": "A1-001",
"blockNumber": 15,
"blockHash": "..."
},
"depthUp": 20,
"depthDown": 2,
"limitChildrenPerNode": 50
}
}
```
Response (смысловые поля):
- `ancestors[]`
- `focus`
- `descendants[]`
---
## 5.4 AddBlock (запись)
Любое изменение (создать канал, пост, reply, реакция, подписка) записывается через:
```json
{
"op": "AddBlock",
"requestId": "...",
"payload": {
"blockchainName": "A1-001",
"blockNumber": 6,
"prevBlockHash": "<64-hex>",
"blockBytesB64": "<base64 full block>"
}
}
```
Важно:
- `blockBytesB64` формируется на клиенте.
- Подпись блока формируется на клиенте приватным blockchain key пользователя.
- Перед добавлением блока клиент берет актуальный курсор цепочки с сервера.
### 6. Типы блоков для каналов и связей (через AddBlock)
- Создание канала: `CreateChannelBody`
- Пост/ответ: `TextBody` (`TEXT_POST`, `TEXT_REPLY`)
- Реакции: `ReactionBody` (лайк/снятие лайка)
- Подписки: `ConnectionBody` (`CONNECTION_FOLLOW`)
### 7. Критерии приемки
- Список каналов отображается с реальными данными API.
- Формат названия канала в UI: `ownerLogin/channelName`.
- Создание канала реально пишет блок и канал появляется после обновления.
- Отправка поста/ответа/реакций реально пишет блок и видна после перечитки API.
- Подписка на пользователя/канал реально пишет блок и отражается в выдаче.
- Переход в тред сообщения показывает реальные `ancestors/focus/descendants`.
- Непрочитанные в списке каналов = общее число сообщений (временное правило).
### 8. Локальный запуск (уже сделано)
Команда:
```bash
./gradlew startLocal
```
Что делает:
- чистит логи;
- билдит сервер;
- запускает локальный WS сервер;
- запускает локальный HTTP сервер клиента;
- открывает браузер по URL с параметром `localWsPort`.

View File

@ -0,0 +1,25 @@
# Краткое описание задачи
Нужно сделать полностью рабочую вкладку «Каналы» в SHiNE.
Пользователь должен:
- видеть список каналов;
- открывать канал и читать сообщения;
- открывать тред сообщения;
- отвечать, ставить и убирать лайк;
- подписываться на пользователей и каналы.
Чтение данных идет через 3 API:
- `ListSubscriptionsFeed`
- `GetChannelMessages`
- `GetMessageThread`
Все действия записи делаются только через `AddBlock` с подписью на клиенте.
Формат имени канала в интерфейсе:
- `имя_пользователя/имя_канала`
Локальный запуск проекта:
```bash
./gradlew startLocal
```

View File

@ -1,203 +0,0 @@
# Анонс для исполнителя (кратко)
Ниже описан текущий статус ветки по функциям:
- PWA + FCM уведомления
- личные сообщения (вторая слева вкладка, "Личные сообщения")
- связи / близкие друзья (центральная вкладка)
- server-push события (в том числе закрытие сессии)
## Что нужно проверить и довести до рабочего состояния
1. **PWA/FCM**: регистрация service worker, получение FCM token, отправка token на сервер, получение foreground/background уведомлений.
2. **Личные сообщения**: отправка первого сообщения любому пользователю, входящие по WS, ACK, fallback в FCM, дедупликация на клиенте.
3. **Связи / близкие друзья**: поиск пользователя, добавление в близкие, обновление графа связей сразу после добавления.
4. **SessionRevoked**: если сессию закрыли с другого устройства, клиент должен корректно завершить локальную сессию и показать понятное состояние.
## Главная цель handoff
Сделать так, чтобы все описанные сценарии стабильно работали end-to-end без ручных "подпинок".
---
# Подробное ТЗ и технические детали
## 1) Архитектура протокола и общие принципы
### 1.1 Формат обмена
Используется JSON-over-WebSocket.
- Для запрос-ответ: `op + requestId + payload`
- Для server-push событий: те же поля, но `event=true`, `requestId` используется как eventId.
### 1.2 ID сообщений/событий
ID генерируются в формате:
- `prefix-yyyyMMdd-HHmmss-SSS-random10`
Реализация: `NetIdGenerator`.
### 1.3 Роли каналов доставки
- **WS** — приоритетная доставка в активные сессии.
- **FCM** — fallback, если ACK по WS не получен или WS-сессия отсутствует.
---
## 2) PWA + FCM
## 2.1 Что уже добавлено
### Клиент
- `shine-UI/manifest.webmanifest`
- `shine-UI/firebase-messaging-sw.js`
- `shine-UI/js/services/pwa-push-service.js`
- `shine-UI/index.html` содержит placeholders:
- `window.__SHINE_FIREBASE_CONFIG__`
- `window.__SHINE_FIREBASE_VAPID_KEY__`
### Сервер
- API `UpsertPushToken`
- Таблица `user_push_tokens`
- Серверный FCM sender (legacy HTTP): `FcmPushSender`
- Конфиг: `fcm.server.key` в `application.properties`
## 2.2 Как должно работать (целевое поведение)
1. Клиент после авторизации регистрирует SW.
2. Просит permission на notifications.
3. Получает FCM token.
4. Если token новый/изменился — отправляет `UpsertPushToken`.
5. Сервер сохраняет token за конкретной сессией.
## 2.3 Что проверить/доделать
- Синхронизация Firebase config между `index.html` и `firebase-messaging-sw.js`.
- Работа iOS Safari (PWA через Home Screen).
- Поведение при смене token.
- Надёжность `FcmPushSender` (таймауты, логирование ошибок ответа).
---
## 3) Вторая слева вкладка: Личные сообщения / Чаты
## 3.1 Продуктовая логика (как должно быть)
1. Открытие списка диалогов на вкладке "Личные сообщения".
2. Кнопка `+` открывает поиск пользователя по префиксу логина (`SearchUsers`).
3. **Первое сообщение можно отправить любому пользователю**, даже если он не контакт.
4. Если пользователь не в контактах — в чате показывается действие "Добавить в контакты".
5. Входящее сообщение может прийти:
- напрямую по WS (`IncomingDirectMessage`)
- через FCM (fallback)
6. На клиенте дедупликация должна быть по `messageId`.
## 3.2 Что уже сделано технически
### Сервер
- `SendDirectMessage`
- `AckIncomingMessage`
- `direct_messages` таблица + DAO
- доставка по активным WS-сессиям + ожидание ACK + fallback в FCM
### Клиент
- `authService.sendDirectMessage(...)`
- `authService.ackIncomingMessage(...)`
- WS client поддерживает server events (`onEvent`)
- В `app.js` обработка `IncomingDirectMessage`
- В `state.js` добавлены `incomingDedup`, `addIncomingMessage`
## 3.3 Что глючит/что проверить
- Стабильность отображения новых чатов, если сообщение пришло от неизвестного пользователя.
- Согласованность списка диалогов и фактических сообщений.
- Поведение при быстрых дубликатах WS + FCM.
- Поведение при сетевых обрывах/повторном коннекте.
---
## 4) Центральная вкладка: Связи / Близкие друзья
## 4.1 Целевое поведение
1. Экран "Связи" загружает граф друзей (`GetUserConnectionsGraph`).
2. Кнопка "Добавить близкого друга" открывает модалку:
- поле логина/префикса
- кнопка "Поиск"
- кнопка "Назад"
3. Поиск идёт через `SearchUsers`.
4. По клику на найденного — подтверждение "Добавить? Да/Нет".
5. При "Да" вызывается `AddCloseFriend`, после успеха:
- закрыть модалку
- вернуться на экран связей
- обновить граф.
## 4.2 Что уже сделано
- UI-flow модалки реализован на `network-view.js`.
- API `AddCloseFriend` добавлен на сервере.
- `ConnectionsStateDAO.upsertRelation(...)` добавлен для upsert связи FRIEND.
## 4.3 Что проверить/доделать
- Валидация edge-cases (добавление самого себя, несуществующий логин).
- Корректность отрисовки графа после добавления.
- Согласованность данных с будущей "настоящей" записью в blockchain (сейчас MVP upsert в `connections_state`).
---
## 5) SessionRevoked и мультисессии
## 5.1 Целевое поведение
Если с другой сессии закрыли текущую сессию:
1. сервер шлёт событие `SessionRevoked`
2. клиент чистит локальную авторизацию
3. клиент возвращается в состояние неавторизованного входа.
## 5.2 Что проверить
- Что событие приходит до закрытия socket.
- Что UX не "зависает" на промежуточном экране.
---
## 6) API-операции (быстрый список)
### Уже используемые/добавленные в ветке
- `SearchUsers`
- `ListContacts`
- `GetUserConnectionsGraph`
- `AddCloseFriend`
- `UpsertPushToken`
- `SendDirectMessage`
- `AckIncomingMessage`
- `CloseActiveSession` (с server event `SessionRevoked`)
---
## 7) Минимальный чек-лист тестирования для нового исполнителя
1. Авторизация пользователя A и B в разных браузерах/устройствах.
2. A отправляет первое сообщение B (без контактов) — должно уйти.
3. B получает по WS, отправляется ACK, fallback FCM не должен дублировать.
4. Выключить WS у B и проверить fallback FCM.
5. Вкладка "Связи": добавить близкого друга через поиск, проверить обновление графа.
6. Закрыть сессию B с другого устройства и проверить `SessionRevoked` UX.
7. Перезапуск клиента: проверка повторной регистрации push-токена только при изменении.
---
## 8) Важное ограничение текущей реализации
Некоторые части реализованы как MVP и требуют стабилизации:
- местами UI/состояние могут рассинхронизироваться;
- fallback WS->FCM может требовать донастройки таймингов и ретраев;
- `AddCloseFriend` сейчас пишет прямое состояние связи (upsert), а не полный blockchain-поток создания блоков.
Это ожидаемо для handoff: задача следующего исполнителя — довести до production-стабильности.
---
## 9) Тестовые логины для быстрой проверки
Для проверки работы системы можно использовать специальные тестовые аккаунты
после выполнения теста `Seed_TestDataPopulation` (он создаёт их через API):
- `A1`
- `A2`
- `A3`
- `A4`
- `A5`
- `A6`
- `A7`
- `A8`
- `A9`
- `A10`
Общий пароль для этих аккаунтов:
- `1`

View File

@ -203,6 +203,7 @@ tasks.register('startLocal', Exec) {
description = "Builds server, starts local WS server and local HTTP UI for end-to-end local testing" description = "Builds server, starts local WS server and local HTTP UI for end-to-end local testing"
dependsOn shadowJar dependsOn shadowJar
dependsOn cleanServerLogs
workingDir = rootDir workingDir = rootDir
def wsPort = System.getProperty("localWsPort", "7070") def wsPort = System.getProperty("localWsPort", "7070")

View File

@ -309,6 +309,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
// Нормализация: -1 не пишем в БД (для совместимости со старым TextBody) // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
if (prevLineNumber != null && prevLineNumber == -1) { if (prevLineNumber != null && prevLineNumber == -1) {
lineCode = null;
prevLineNumber = null; prevLineNumber = null;
prevLineHash32 = null; prevLineHash32 = null;
thisLineNumber = null; thisLineNumber = null;

View File

@ -1,31 +0,0 @@
# Задача 2 — Проверка работы личных данных и статусов профиля (правая вкладка)
## Что реализовано
- На правой вкладке `Профиль` отображаются реальные пользовательские параметры, загружаемые через `ListUserParams`.
- Поля профиля:
- `first_name` (чтение с обратной совместимостью с `name`)
- `last_name`
- `address_physical`
- `address_web`
- `phone`
- Кнопка `Обновить` открывает форму редактирования и сохраняет изменения в пользовательские параметры блокчейна.
- Добавлены рабочие переключатели:
- `official`
- `shine`
- Для `official`/`shine` используется подтверждение перед записью, с предупреждением, что изменение идёт через блокчейн-параметры и требует подписи ключом пользователя.
- Если `official`/`shine` отсутствуют в параметрах, они считаются `no` по умолчанию.
## Что проверить вручную
1. Авторизоваться и открыть правую вкладку `Профиль`.
2. Убедиться, что поля профиля читаются из `ListUserParams`, а не из заглушек.
3. Нажать `Обновить`, изменить `first_name/last_name/address_physical/address_web/phone`, нажать `Сохранить`.
4. Убедиться, что после сохранения данные перечитались и обновились на экране.
5. Нажать `Официальный`, подтвердить изменение и проверить смену `no -> yes` (или `yes -> no`).
6. Нажать `Сияющий`, подтвердить изменение и проверить смену `no -> yes` (или `yes -> no`).
7. Обновить страницу и убедиться, что состояния `official/shine` и личные поля сохраняются.
8. Проверить кейс отсутствия `official/shine` в истории: UI должен показывать `no`.
## Ожидаемый результат
- Правая вкладка профиля работает с реальными данными пользователя.
- `official` и `shine` работают как настоящие параметры (yes/no), а не заглушки.
- После каждой записи UI делает повторный `ListUserParams` и показывает актуальное состояние.