SHA256
Compare commits
107
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
823a41c027 | ||
|
|
2a834f1b14 | ||
|
|
c8ffb6cf29 | ||
|
|
ecc9efd434 | ||
|
|
dd35e56029 | ||
|
|
d0e7998650 | ||
|
|
fec5e49304 | ||
|
|
3b12e14e71 | ||
|
|
86eaf2139d | ||
|
|
65fad993ad | ||
|
|
55e6e477be | ||
|
|
ba5efcc152 | ||
|
|
26253564d5 | ||
|
|
92791c77a9 | ||
|
|
465792b2ab | ||
|
|
de269fd828 | ||
|
|
8c91484f37 | ||
|
|
6904ac8b7c | ||
|
|
aea6bbcb0e | ||
|
|
a788d8bcf5 | ||
|
|
cc074a941f | ||
|
|
47574100f9 | ||
|
|
7ad74942e0 | ||
|
|
ac1cc04637 | ||
|
|
722d055e2d | ||
|
|
a9a55da8e0 | ||
|
|
f2b23ace8b | ||
|
|
1f2048e270 | ||
|
|
b16a23243e | ||
|
|
653f1268a6 | ||
|
|
56db6d0add | ||
|
|
cf2152dcfc | ||
|
|
a95bd245cf | ||
|
|
92fd315505 | ||
|
|
2225c2d173 | ||
|
|
f8a76bcd7f | ||
|
|
3efa8bb7ee | ||
|
|
5c155ef503 | ||
|
|
41d199e24a | ||
|
|
e1f2b54de3 | ||
|
|
d6c5757dfa | ||
|
|
9a489801c5 | ||
|
|
9fcdcd087b | ||
|
|
af1304022e | ||
|
|
7972676eb8 | ||
|
|
bef205aec7 | ||
|
|
49fdbbf7ae | ||
|
|
dd69a52273 | ||
|
|
c681b4d684 | ||
|
|
b166013707 | ||
|
|
3e04727022 | ||
|
|
5d13112b00 | ||
|
|
373f88086e | ||
|
|
05492306c0 | ||
|
|
423d490939 | ||
|
|
7edc0ba901 | ||
|
|
0ebb71daf1 | ||
|
|
b4480d89cf | ||
|
|
ff584ba5d1 | ||
|
|
4b15cabd4f | ||
|
|
be4a2d135a | ||
|
|
69f0fdf120 | ||
|
|
e3bebff618 | ||
|
|
f19f7b0ec4 | ||
|
|
ca4cfd9d8d | ||
|
|
96d292074b | ||
|
|
0536a018c6 | ||
|
|
81d1b84a7d | ||
|
|
61c21b245e | ||
|
|
919387f581 | ||
|
|
3b8ea70d3c | ||
|
|
477ab3b580 | ||
|
|
a1da814030 | ||
|
|
19fd5611b2 | ||
|
|
556004a557 | ||
|
|
fba6d6bba0 | ||
|
|
04252e006b | ||
|
|
436e1f0c53 | ||
|
|
21030b1d51 | ||
|
|
b583a86ade | ||
|
|
3262ec9b4a | ||
|
|
0c9afea67a | ||
|
|
b83543d018 | ||
|
|
d4a0185507 | ||
|
|
42dcf6970d | ||
|
|
0b4374141e | ||
|
|
652ddc9d88 | ||
|
|
cf6a2830c8 | ||
|
|
01d9553db4 | ||
|
|
578b526f96 | ||
|
|
e3061b46f9 | ||
|
|
2559f1e66b | ||
|
|
519bce6b78 | ||
|
|
557ea96be0 | ||
|
|
9a49cc67f0 | ||
|
|
3012f0799b | ||
|
|
7a8852f64b | ||
|
|
f92e6c3cf1 | ||
|
|
72dc83daff | ||
|
|
04d9d588e8 | ||
|
|
345a21a211 | ||
|
|
3de992d251 | ||
|
|
369ef61cab | ||
|
|
e6e96c4b0d | ||
|
|
dc96033cb1 | ||
|
|
9ee6bf4380 | ||
|
|
e0f0726e68 |
+17
@@ -76,10 +76,21 @@ shine-solana/shine/scripts/**/*.env
|
|||||||
shine-solana/shine/scripts/**/TEMP_*.md
|
shine-solana/shine/scripts/**/TEMP_*.md
|
||||||
|
|
||||||
# Локальные артефакты и внешние материалы ESP32-подпроекта
|
# Локальные артефакты и внешние материалы ESP32-подпроекта
|
||||||
|
ESP32/esp32-config-tool/
|
||||||
ESP32/**/.git/
|
ESP32/**/.git/
|
||||||
ESP32/**/.idea/
|
ESP32/**/.idea/
|
||||||
|
ESP32-wallet/.idea/
|
||||||
ESP32/**/.arduino-build/
|
ESP32/**/.arduino-build/
|
||||||
ESP32/**/official-demo/
|
ESP32/**/official-demo/
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/
|
||||||
|
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/**
|
||||||
|
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/lv_conf.h
|
||||||
ESP32/**/original-firmware/*.bin
|
ESP32/**/original-firmware/*.bin
|
||||||
ESP32/**/original-firmware/*.bin.sha256
|
ESP32/**/original-firmware/*.bin.sha256
|
||||||
ESP32/**/*.elf
|
ESP32/**/*.elf
|
||||||
@@ -93,3 +104,9 @@ ESP32/**/*.a
|
|||||||
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
||||||
server-backup/archive/**
|
server-backup/archive/**
|
||||||
!server-backup/archive/.gitkeep
|
!server-backup/archive/.gitkeep
|
||||||
|
|
||||||
|
# Локальная дев-обвязка Claude (дев-сервер shine-UI, сессии, планы) — не коммитим
|
||||||
|
.claude/
|
||||||
|
# Рабочие бэкапы/превью-ассеты UI — не для репозитория
|
||||||
|
*.bak.png
|
||||||
|
shine-UI/assets/navbar_preview.png
|
||||||
|
|||||||
Generated
+1
-3
@@ -2,7 +2,5 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo" vcs="Git" />
|
|
||||||
<mapping directory="$PROJECT_DIR$/tools/understand-anything-lab/upstream" vcs="Git" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -24,12 +24,12 @@
|
|||||||
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
||||||
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
||||||
|
|
||||||
## ESP32 UI сабсервера
|
## ESP32 UI homeserver
|
||||||
- Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными.
|
- Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными.
|
||||||
- Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча.
|
- Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча.
|
||||||
- При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч.
|
- При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч.
|
||||||
- При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение.
|
- При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение.
|
||||||
- Для нового ESP32 UI-прототипа сабсервера использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
|
- Для нового ESP32 UI-прототипа homeserver использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
|
||||||
|
|
||||||
## Solana-модуль
|
## Solana-модуль
|
||||||
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.
|
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.
|
||||||
@@ -83,14 +83,26 @@
|
|||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
|
- Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`.
|
||||||
- Базовый целевой хост для деплоя по умолчанию: `player@93.170.12.154` (`shineup.me`).
|
- Production-хост SHiNE: `player@shineup.me` (`185.229.109.118`).
|
||||||
|
- Основной test-хост SHiNE: `player@193.8.215.70` (`test2.shineup.me`).
|
||||||
|
- Резервный test-хост SHiNE: `player@93.170.12.154` (`test.shineup.me`).
|
||||||
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
||||||
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
||||||
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||||
- Для серверного деплоя использовать один gradle task: `./gradlew deployServer`.
|
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя.
|
||||||
- Для UI деплоя использовать один gradle task: `./gradlew deployUI`.
|
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `test2.shineup.me`.
|
||||||
|
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`.
|
||||||
|
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`.
|
||||||
|
- Production server deploy: `./gradlew deployServerProduction`.
|
||||||
|
- Production UI deploy: `./gradlew deployUIProduction`.
|
||||||
|
- Резервный test deploy на `test.shineup.me`: `./gradlew deployServerTest` и `./gradlew deployUITest`, но пока их не использовать без отдельной причины.
|
||||||
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
||||||
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
||||||
|
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
|
||||||
|
- Для продовой настройки тестового Arweave-кошелька JWK-файл нужно хранить только на сервере, например: `/home/player/SHiNE/secrets/test-free-avatar-wallet.json`.
|
||||||
|
- Для этой временной фичи на проде должны быть заданы параметры `test.freeAvatar.walletJwkPath` и `test.freeAvatar.walletAddress` через серверный override-конфиг/секреты на хосте.
|
||||||
|
- После изменения продовых значений `test.freeAvatar.*` нужно заново выполнить серверный деплой или перезапуск сервера, чтобы настройки были перечитаны приложением.
|
||||||
|
- При таких изменениях в git допускается коммитить только документацию и код чтения настроек, но не сам JWK, не содержимое секрета и не реальные приватные ключи.
|
||||||
|
|
||||||
## Логи звонков (установка соединения)
|
## Логи звонков (установка соединения)
|
||||||
- Специальный поток диагностики установки звонков идёт через `CallDeliveryReport` (клиент → сервер).
|
- Специальный поток диагностики установки звонков идёт через `CallDeliveryReport` (клиент → сервер).
|
||||||
|
|||||||
@@ -155,11 +155,11 @@
|
|||||||
|
|
||||||
- это обязательный шаг перед переходом от "собрали" к "доверяем".
|
- это обязательный шаг перед переходом от "собрали" к "доверяем".
|
||||||
|
|
||||||
### 3. Устройство на ESP32 как сабсервер с ключами
|
### 3. Устройство на ESP32 как homeserver с ключами
|
||||||
|
|
||||||
Что сделать:
|
Что сделать:
|
||||||
|
|
||||||
- дописать прошивку, чтобы устройство могло выступать сабсервером с ключами;
|
- дописать прошивку, чтобы устройство могло выступать homeserver с ключами;
|
||||||
- дать ему возможность регистрироваться и подключаться к серверу;
|
- дать ему возможность регистрироваться и подключаться к серверу;
|
||||||
- определить, какие операции устройство подписывает и где хранит ключевой материал.
|
- определить, какие операции устройство подписывает и где хранит ключевой материал.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Этот файл описывает именно этапы авторизации клиента, то есть как создать новую сессию и как войти в уже существующую.
|
Этот файл описывает именно этапы авторизации клиента, то есть как создать новую сессию и как войти в уже существующую.
|
||||||
|
|
||||||
Здесь четыре метода:
|
Здесь четыре базовых метода обычной авторизации:
|
||||||
|
|
||||||
- `AuthChallenge`
|
- `AuthChallenge`
|
||||||
- `CreateAuthSession`
|
- `CreateAuthSession`
|
||||||
@@ -17,8 +17,35 @@
|
|||||||
- на втором шаге клиент присылает подписанный ответ;
|
- на втором шаге клиент присылает подписанный ответ;
|
||||||
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
|
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
|
||||||
|
|
||||||
|
Новые поля этого раздела:
|
||||||
|
|
||||||
|
- `sessionType` — числовой код типа сессии;
|
||||||
|
- `clientPlatform` — свободная строка платформы клиента.
|
||||||
|
|
||||||
|
Текущие поддерживаемые коды `sessionType`:
|
||||||
|
|
||||||
|
- `1` — обычный клиент;
|
||||||
|
- `50` — кошелёк;
|
||||||
|
- `100` — homeserver.
|
||||||
|
|
||||||
|
Правило проверки `sessionType`:
|
||||||
|
|
||||||
|
1. если в `Solana PDA` нет записи для `sessionKey`, сервер принимает `sessionType`, присланный клиентом;
|
||||||
|
2. если запись в `PDA` есть, `sessionType` в запросе должен совпадать с `session_type` из `PDA`;
|
||||||
|
3. при несовпадении сервер возвращает `460 / SESSION_TYPE_MISMATCH`.
|
||||||
|
|
||||||
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
|
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
|
||||||
|
|
||||||
|
Отдельно появился новый серверный сценарий pairing через доверенный homeserver/ESP. Он не заменяет обычный вход и описан в:
|
||||||
|
|
||||||
|
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
|
||||||
|
|
||||||
|
Кратко:
|
||||||
|
|
||||||
|
- `AuthChallenge/CreateAuthSession` и `SessionChallenge/SessionLogin` остаются каноническими потоками обычной авторизации;
|
||||||
|
- pairing через ESP идёт отдельными `op` и только подготавливает безопасное добавление новой сессии;
|
||||||
|
- решение об одобрении pairing принимает любая уже авторизованная доверенная сессия пользователя.
|
||||||
|
|
||||||
## 1. Поток авторизации
|
## 1. Поток авторизации
|
||||||
|
|
||||||
Поддерживаются два сценария:
|
Поддерживаются два сценария:
|
||||||
@@ -94,6 +121,8 @@ ed25519/BASE64_PUBLIC_KEY
|
|||||||
"authNonce": "nonce",
|
"authNonce": "nonce",
|
||||||
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
||||||
"signatureB64": "BASE64_SIGNATURE",
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
"clientInfo": "Android 15; Pixel 9"
|
"clientInfo": "Android 15; Pixel 9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +182,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
|||||||
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
|
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
|
||||||
- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере.
|
- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере.
|
||||||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
|
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||||||
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
|
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
|
||||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
@@ -208,6 +239,8 @@ AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
|||||||
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
"timeMs": 1774600010456,
|
"timeMs": 1774600010456,
|
||||||
"signatureB64": "BASE64_SIGNATURE",
|
"signatureB64": "BASE64_SIGNATURE",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
"clientInfo": "Android 15; Pixel 9"
|
"clientInfo": "Android 15; Pixel 9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,12 +291,40 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
|||||||
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
|
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
|
||||||
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
|
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
|
||||||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||||||
|
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||||||
|
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||||||
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
|
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
|
||||||
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
|
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
|
||||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 6. Pairing через homeserver/ESP
|
||||||
|
|
||||||
|
Новые `op`, относящиеся к этому сценарию:
|
||||||
|
|
||||||
|
- `GetTrustedDeviceLoginSettings`
|
||||||
|
- `UpsertTrustedDeviceLoginSettings`
|
||||||
|
- `StartTrustedDeviceLogin`
|
||||||
|
- `ListTrustedDeviceLoginRequests`
|
||||||
|
- `ApproveTrustedDeviceLogin`
|
||||||
|
- `RejectTrustedDeviceLogin`
|
||||||
|
- `CancelTrustedDeviceLogin`
|
||||||
|
- `GetTrustedDeviceLoginStatus`
|
||||||
|
|
||||||
|
В этом потоке:
|
||||||
|
|
||||||
|
- новое устройство не владеет `deviceKey` и не проходит обычный `CreateAuthSession`;
|
||||||
|
- пароль проверяется сервером только как фильтр;
|
||||||
|
- решение об одобрении принимает уже авторизованная доверенная сессия пользователя;
|
||||||
|
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
|
||||||
|
|
||||||
|
Точные форматы этих операций см. в `03_Session_Management_API.md` и в протокольном документе:
|
||||||
|
|
||||||
|
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. Пример ошибки
|
## 6. Пример ошибки
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -7,6 +7,20 @@
|
|||||||
- `ListSessions` — получить список активных сессий пользователя;
|
- `ListSessions` — получить список активных сессий пользователя;
|
||||||
- `CloseActiveSession` — закрыть одну из активных сессий.
|
- `CloseActiveSession` — закрыть одну из активных сессий.
|
||||||
|
|
||||||
|
Дополнительно в этом же слое управления сессиями появился сценарий pairing через доверенную уже авторизованную сессию пользователя:
|
||||||
|
|
||||||
|
- `GetTrustedDeviceLoginSettings`
|
||||||
|
- `UpsertTrustedDeviceLoginSettings`
|
||||||
|
- `ListTrustedDeviceLoginRequests`
|
||||||
|
- `ApproveTrustedDeviceLogin`
|
||||||
|
- `RejectTrustedDeviceLogin`
|
||||||
|
- `CancelTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Анонимное новое устройство работает с двумя связанными операциями:
|
||||||
|
|
||||||
|
- `StartTrustedDeviceLogin`
|
||||||
|
- `GetTrustedDeviceLoginStatus`
|
||||||
|
|
||||||
Логика раздела такая:
|
Логика раздела такая:
|
||||||
|
|
||||||
- сначала пользователь проходит `SessionLogin`;
|
- сначала пользователь проходит `SessionLogin`;
|
||||||
@@ -42,6 +56,9 @@
|
|||||||
"sessions": [
|
"sessions": [
|
||||||
{
|
{
|
||||||
"sessionId": "sess_7c5e5c4b",
|
"sessionId": "sess_7c5e5c4b",
|
||||||
|
"sessionType": 1,
|
||||||
|
"clientPlatform": "Web",
|
||||||
|
"onlineOnThisServer": true,
|
||||||
"clientInfoFromClient": "Android 15; Pixel 9",
|
"clientInfoFromClient": "Android 15; Pixel 9",
|
||||||
"clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1",
|
"clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1",
|
||||||
"geo": "RU/Moscow",
|
"geo": "RU/Moscow",
|
||||||
@@ -58,6 +75,20 @@
|
|||||||
- `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий.
|
- `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий.
|
||||||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
### Поля одной сессии в `ListSessions`
|
||||||
|
|
||||||
|
- `sessionId` — идентификатор активной сессии;
|
||||||
|
- `sessionType` — числовой код типа сессии:
|
||||||
|
- `1` — клиент;
|
||||||
|
- `50` — кошелёк;
|
||||||
|
- `100` — homeserver;
|
||||||
|
- `clientPlatform` — строка платформы, как её прислал клиент;
|
||||||
|
- `onlineOnThisServer` — `true`, если эта сессия сейчас держит живое WebSocket-подключение именно к данному серверу;
|
||||||
|
- `clientInfoFromClient` — краткая строка клиента;
|
||||||
|
- `clientInfoFromRequest` — строка, собранная сервером из запроса;
|
||||||
|
- `geo` — страна/город или fallback-строка;
|
||||||
|
- `lastAuthenticatedAtMs` — время последней успешной авторизации этой сессии.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. `CloseActiveSession`
|
## 2. `CloseActiveSession`
|
||||||
@@ -134,3 +165,321 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
|
|||||||
|
|
||||||
Важно: это **не человеко-читаемое имя**, а непрозрачный идентификатор.
|
Важно: это **не человеко-читаемое имя**, а непрозрачный идентификатор.
|
||||||
Нужно передавать его как есть, без нормализации регистра и без URL-экранирования внутри JSON.
|
Нужно передавать его как есть, без нормализации регистра и без URL-экранирования внутри JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. TrustedDeviceLogin через доверенную сессию
|
||||||
|
|
||||||
|
Этот блок относится к сценарию добавления новой сессии через доверенное устройство пользователя.
|
||||||
|
|
||||||
|
### 5.1. `GetTrustedDeviceLoginSettings`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "trusted-login-get-001",
|
||||||
|
"payload": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "trusted-login-get-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"hasPassword": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если отдельной записи настроек на сервере ещё нет, сервер считает состояние по умолчанию таким:
|
||||||
|
|
||||||
|
- `enabled = true`
|
||||||
|
- `hasPassword = false`
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### 5.2. `UpsertTrustedDeviceLoginSettings`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "esp-set-001",
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"passwordHash": "sha256$0123abcd..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если вход через доверенное устройство должен работать **без доп. пароля**, клиент включает его с пустым `passwordHash`.
|
||||||
|
|
||||||
|
Если `enabled = false`, сервер автоматически удаляет пароль и запрещает вход через другое устройство.
|
||||||
|
|
||||||
|
Формат непустого `passwordHash`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "UpsertTrustedDeviceLoginSettings",
|
||||||
|
"requestId": "esp-set-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"hasPassword": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### 5.3. `StartTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Эта операция доступна без уже существующей пользовательской сессии.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "StartTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-start-001",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice",
|
||||||
|
"passwordHash": "sha256$0123abcd...",
|
||||||
|
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
|
"requesterSessionType": 1,
|
||||||
|
"requesterClientPlatform": "Android",
|
||||||
|
"payloadType": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если на доверённом устройстве вход включён **без доп. пароля**, новое устройство может отправить пустой `passwordHash`.
|
||||||
|
|
||||||
|
Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку.
|
||||||
|
Поле `shortCode` теперь содержит `10` цифр. В UI его рекомендуется показывать как `5` пар, например: `49 20 70 91 23`.
|
||||||
|
|
||||||
|
TTL заявки фиксирован на сервере и сейчас всегда равен `300` секундам.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "StartTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-start-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "created",
|
||||||
|
"shortCode": "4920709123",
|
||||||
|
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
|
||||||
|
"expiresAtMs": 1781441990538,
|
||||||
|
"trustedSessionOnline": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / EMPTY_LOGIN`
|
||||||
|
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
||||||
|
- `400 / BAD_REQUESTER_SESSION_KEY`
|
||||||
|
- `400 / BAD_SESSION_TYPE`
|
||||||
|
- `400 / BAD_PAYLOAD_TYPE`
|
||||||
|
- `422 / PAIRING_NOT_AVAILABLE`
|
||||||
|
- `422 / PAIRING_PASSWORD_INVALID` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля.
|
||||||
|
- `422 / PAIRING_NO_TRUSTED_SESSION_ONLINE` — сейчас нет ни одной онлайн доверённой сессии пользователя, поэтому код не создаётся.
|
||||||
|
- `429 / PAIRING_RATE_LIMITED`
|
||||||
|
|
||||||
|
### 5.4. `ListTrustedDeviceLoginRequests`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
Возвращает только реально активные pending-заявки со `state = created`. Уже `approved` и `rejected` заявки в этот список больше не попадают.
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListTrustedDeviceLoginRequests",
|
||||||
|
"requestId": "esp-list-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"requests": [
|
||||||
|
{
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "created",
|
||||||
|
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||||||
|
"requesterSessionType": 1,
|
||||||
|
"requesterClientPlatform": "Android",
|
||||||
|
"payloadType": 1,
|
||||||
|
"shortCode": "4920709123",
|
||||||
|
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
|
||||||
|
"createdAtMs": 1781441810538,
|
||||||
|
"expiresAtMs": 1781441990538,
|
||||||
|
"deliveredToHomeserver": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION`
|
||||||
|
|
||||||
|
### 5.5. `ApproveTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ApproveTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-approve-001",
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"encryptedPayload": "BASE64_OR_OTHER_OPAQUE_PAYLOAD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ApproveTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-approve-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "approved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / EMPTY_PAIRING_ID`
|
||||||
|
- `400 / EMPTY_ENCRYPTED_PAYLOAD`
|
||||||
|
- `404 / PAIRING_NOT_FOUND`
|
||||||
|
- `422 / PAIRING_OF_ANOTHER_USER`
|
||||||
|
- `422 / PAIRING_NOT_PENDING`
|
||||||
|
- `422 / PAIRING_EXPIRED`
|
||||||
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION`
|
||||||
|
|
||||||
|
### 5.6. `RejectTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Доступно для любой уже авторизованной доверенной сессии пользователя. Похоже на approve, но переводит заявку в `state=rejected`.
|
||||||
|
|
||||||
|
### 5.7. `GetTrustedDeviceLoginStatus`
|
||||||
|
|
||||||
|
Операция для нового устройства.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginStatus",
|
||||||
|
"requestId": "esp-status-001",
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ после approve
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetTrustedDeviceLoginStatus",
|
||||||
|
"requestId": "esp-status-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "approved",
|
||||||
|
"shortCode": "4920709123",
|
||||||
|
"fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA",
|
||||||
|
"payloadType": 1,
|
||||||
|
"encryptedPayload": "AQIDBA==",
|
||||||
|
"expiresAtMs": 1781441990538
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Возможные `state`
|
||||||
|
|
||||||
|
- `created`
|
||||||
|
- `approved`
|
||||||
|
- `rejected`
|
||||||
|
- `canceled`
|
||||||
|
- `expired`
|
||||||
|
|
||||||
|
### 5.8. `CancelTrustedDeviceLogin`
|
||||||
|
|
||||||
|
Операция для нового устройства, которое уже создало pairing-заявку и хочет принудительно снять ожидание до истечения TTL.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CancelTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-cancel-001",
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "CancelTrustedDeviceLogin",
|
||||||
|
"requestId": "esp-cancel-001",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"pairingId": "base64url",
|
||||||
|
"state": "canceled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `400 / EMPTY_PAIRING_ID`
|
||||||
|
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
||||||
|
- `400 / BAD_REQUESTER_SESSION_KEY`
|
||||||
|
- `404 / PAIRING_NOT_FOUND`
|
||||||
|
- `422 / PAIRING_OF_ANOTHER_REQUESTER`
|
||||||
|
- `422 / PAIRING_NOT_PENDING`
|
||||||
|
|||||||
@@ -15,10 +15,20 @@
|
|||||||
| `AddUser` | `01_User_Registration_API.md` | отключено (`410 / ADD_USER_DISABLED`) |
|
| `AddUser` | `01_User_Registration_API.md` | отключено (`410 / ADD_USER_DISABLED`) |
|
||||||
| `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя + server-состояние его блокчейна |
|
| `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя + server-состояние его блокчейна |
|
||||||
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
|
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
|
||||||
|
| `TestGetFreeAvatarQuota` | `14_Test_Free_Avatar_Upload_API.md` | временный тестовый просмотр остатка бесплатных загрузок аватара |
|
||||||
|
| `TestUploadFreeAvatar` | `14_Test_Free_Avatar_Upload_API.md` | временная тестовая бесплатная загрузка маленького аватара в Arweave |
|
||||||
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
|
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
|
||||||
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
|
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
|
||||||
| `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию |
|
| `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию |
|
||||||
| `SessionLogin` | `02_Authentication_API.md` | вход в существующую сессию |
|
| `SessionLogin` | `02_Authentication_API.md` | вход в существующую сессию |
|
||||||
|
| `GetTrustedDeviceLoginSettings` | `03_Session_Management_API.md` | чтение текущего режима входа через доверенное устройство |
|
||||||
|
| `UpsertTrustedDeviceLoginSettings` | `03_Session_Management_API.md` | включение/обновление pairing-настроек доверенной сессией |
|
||||||
|
| `StartTrustedDeviceLogin` | `03_Session_Management_API.md` | создание pairing-заявки для нового устройства |
|
||||||
|
| `ListTrustedDeviceLoginRequests` | `03_Session_Management_API.md` | список активных pairing-заявок для доверенной сессии |
|
||||||
|
| `ApproveTrustedDeviceLogin` | `03_Session_Management_API.md` | подтверждение pairing-заявки доверенной сессией |
|
||||||
|
| `RejectTrustedDeviceLogin` | `03_Session_Management_API.md` | отклонение pairing-заявки доверенной сессией |
|
||||||
|
| `CancelTrustedDeviceLogin` | `03_Session_Management_API.md` | отмена pairing-заявки со стороны нового ожидающего устройства |
|
||||||
|
| `GetTrustedDeviceLoginStatus` | `03_Session_Management_API.md` | чтение статуса и результата pairing-заявки |
|
||||||
| `ListSessions` | `03_Session_Management_API.md` | список активных сессий |
|
| `ListSessions` | `03_Session_Management_API.md` | список активных сессий |
|
||||||
| `CloseActiveSession` | `03_Session_Management_API.md` | закрытие активной сессии |
|
| `CloseActiveSession` | `03_Session_Management_API.md` | закрытие активной сессии |
|
||||||
| `AddBlock` | `04_Add_Block_to_Blockchain_API.md` | добавление блока в блокчейн |
|
| `AddBlock` | `04_Add_Block_to_Blockchain_API.md` | добавление блока в блокчейн |
|
||||||
@@ -54,5 +64,6 @@
|
|||||||
## Важные замечания
|
## Важные замечания
|
||||||
|
|
||||||
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
||||||
|
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||||
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
||||||
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# API для разработчиков: DM, push и сигналы звонков
|
# API для разработчиков: DM, push и сигналы звонков
|
||||||
|
|
||||||
Документ описывает WebSocket-операции для подписанных личных сообщений, WebPush и realtime-сигналов звонков.
|
Документ описывает публичные операции, связанные с личными сообщениями, WebPush и сигналами звонков.
|
||||||
|
|
||||||
Логика личных сообщений дополнительно описана в `Dev_Docs/Personal_Messages/README.md`; этот файл фиксирует именно публичные `op`, поля запросов и поля ответов.
|
Подробная логика DM и бинарного формата:
|
||||||
|
|
||||||
|
- `Dev_Docs/Personal_Messages/README.md`
|
||||||
|
|
||||||
## 1. `UpsertPushToken`
|
## 1. `UpsertPushToken`
|
||||||
|
|
||||||
@@ -40,11 +42,9 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `SendTestWebPush`
|
## 2. `SendTestWebPush`
|
||||||
|
|
||||||
Требует авторизации. Если `login` передан, он должен совпадать с логином текущей сессии.
|
Требует авторизации.
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
@@ -61,65 +61,18 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Успешный ответ
|
## 3. `SendMessagePair` и `ReceiveOutcomingMessage`
|
||||||
|
|
||||||
```json
|
`ReceiveOutcomingMessage` — алиас `SendMessagePair`.
|
||||||
{
|
|
||||||
"op": "SendTestWebPush",
|
|
||||||
"requestId": "push-test-001",
|
|
||||||
"status": 200,
|
|
||||||
"ok": true,
|
|
||||||
"payload": {
|
|
||||||
"targetLogin": "alice",
|
|
||||||
"attemptedSessions": 1,
|
|
||||||
"sessionsWithPushConfig": 1,
|
|
||||||
"delivered": 1,
|
|
||||||
"failed": 0,
|
|
||||||
"sentAtMs": 1774700000123
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
### Назначение
|
||||||
|
|
||||||
## 3. `SendDirectMessage`
|
Передаёт пару signed DM-блоков:
|
||||||
|
|
||||||
Отправляет один подписанный DM-пакет.
|
- `incomingBlobB64` — блок `type=1` или `type=3`
|
||||||
|
- `outgoingBlobB64` — блок `type=2` или `type=4`
|
||||||
|
|
||||||
### Запрос
|
Для контентных сообщений `type=1/2` внутри base64 лежит бинарный формат `SHiNE_DM`.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "SendDirectMessage",
|
|
||||||
"requestId": "dm-001",
|
|
||||||
"payload": {
|
|
||||||
"blobB64": "BASE64_SIGNED_DM_PACKET"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Успешный ответ
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "SendDirectMessage",
|
|
||||||
"requestId": "dm-001",
|
|
||||||
"status": 200,
|
|
||||||
"ok": true,
|
|
||||||
"payload": {
|
|
||||||
"messageId": "dm-1",
|
|
||||||
"deliveredWsSessions": 1,
|
|
||||||
"deliveredWebPushSessions": 0,
|
|
||||||
"sessionNotFound": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. `SendMessagePair` и `ReceiveOutcomingMessage`
|
|
||||||
|
|
||||||
`ReceiveOutcomingMessage` сейчас является алиасом `SendMessagePair` и использует тот же request/handler.
|
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
@@ -143,20 +96,31 @@
|
|||||||
"status": 200,
|
"status": 200,
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"payload": {
|
"payload": {
|
||||||
"baseKey": "base-key",
|
"baseKey": "from|to|time|nonce",
|
||||||
"incomingKey": "incoming-key",
|
"incomingKey": "from|to|time|nonce|1",
|
||||||
"outgoingKey": "outgoing-key",
|
"outgoingKey": "from|to|time|nonce|2",
|
||||||
"deliveredWsSessions": 1,
|
"deliveredWsSessions": 1,
|
||||||
"deliveredWebPushSessions": 0
|
"deliveredWebPushSessions": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Ошибки
|
||||||
|
|
||||||
## 5. `ReceiveIncomingMessage`
|
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
|
||||||
|
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
|
||||||
|
- `400 / BAD_CONTENT_FORMAT` — для контентного сообщения пришёл не `SHiNE_DM`
|
||||||
|
- `400 / ATTACHMENTS_DISABLED` — в `SHiNE_DM` пришёл `attachmentsCount != 0`
|
||||||
|
- `404 / USER_NOT_FOUND` — один из логинов не найден
|
||||||
|
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
|
||||||
|
|
||||||
Принимает входящий подписанный DM-блок.
|
## 4. `ReceiveIncomingMessage`
|
||||||
|
|
||||||
|
Принимает только один входящий signed DM-блок.
|
||||||
|
|
||||||
|
### Назначение
|
||||||
|
|
||||||
|
Используется там, где нужно принять только incoming-вариант сообщения.
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
@@ -170,28 +134,9 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Успешный ответ
|
## 5. `AckSessionDelivery`
|
||||||
|
|
||||||
```json
|
Требует авторизации. Подтверждает доставку в текущую сессию.
|
||||||
{
|
|
||||||
"op": "ReceiveIncomingMessage",
|
|
||||||
"requestId": "dm-in-001",
|
|
||||||
"status": 200,
|
|
||||||
"ok": true,
|
|
||||||
"payload": {
|
|
||||||
"messageKey": "incoming-key",
|
|
||||||
"baseKey": "base-key",
|
|
||||||
"deliveredWsSessions": 1,
|
|
||||||
"deliveredWebPushSessions": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. `AckSessionDelivery`
|
|
||||||
|
|
||||||
Требует авторизации. Подтверждает доставку сообщения в текущую сессию.
|
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
@@ -200,107 +145,46 @@
|
|||||||
"op": "AckSessionDelivery",
|
"op": "AckSessionDelivery",
|
||||||
"requestId": "ack-001",
|
"requestId": "ack-001",
|
||||||
"payload": {
|
"payload": {
|
||||||
"messageKey": "incoming-key"
|
"messageKey": "from|to|time|nonce|1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Успешный ответ
|
## 6. Событие `SignedMessageArrived`
|
||||||
|
|
||||||
|
Сервер присылает его по WebSocket в активные сессии адресата.
|
||||||
|
|
||||||
|
### Payload события
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"op": "AckSessionDelivery",
|
"messageKey": "from|to|time|nonce|1",
|
||||||
"requestId": "ack-001",
|
"baseKey": "from|to|time|nonce",
|
||||||
"status": 200,
|
"fromLogin": "alice",
|
||||||
"ok": true,
|
"toLogin": "bob",
|
||||||
"payload": {
|
"targetLogin": "bob",
|
||||||
"messageKey": "incoming-key"
|
"messageType": 1,
|
||||||
}
|
"timeMs": 1774700000123,
|
||||||
|
"nonce": 123456789,
|
||||||
|
"blobB64": "BASE64_SIGNED_BLOCK",
|
||||||
|
"backlog": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Если это новая ревизия того же письма, `messageKey` остаётся тем же, а `revisionTimeMs` меняется внутри бинарного блока.
|
||||||
|
|
||||||
## 7. `CallInviteBroadcast`
|
## 7. `CallInviteBroadcast`
|
||||||
|
|
||||||
Требует авторизации. Отправляет приглашение к звонку на активные сессии пользователя `toLogin`.
|
Требует авторизации. Шлёт приглашение к звонку в активные сессии `toLogin`.
|
||||||
|
|
||||||
### Запрос
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "CallInviteBroadcast",
|
|
||||||
"requestId": "call-invite-001",
|
|
||||||
"payload": {
|
|
||||||
"toLogin": "bob",
|
|
||||||
"callId": "call-1",
|
|
||||||
"type": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Успешный ответ
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "CallInviteBroadcast",
|
|
||||||
"requestId": "call-invite-001",
|
|
||||||
"status": 200,
|
|
||||||
"ok": true,
|
|
||||||
"payload": {
|
|
||||||
"callId": "call-1",
|
|
||||||
"deliveredWsSessions": 1,
|
|
||||||
"deliveredFcmSessions": 0,
|
|
||||||
"deliveredWebPushSessions": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. `CallSignalToSession`
|
## 8. `CallSignalToSession`
|
||||||
|
|
||||||
Требует авторизации. Отправляет сигнал звонка в конкретную сессию получателя.
|
Требует авторизации. Шлёт сигнал звонка в конкретную сессию.
|
||||||
|
|
||||||
### Запрос
|
## 9. Замечания
|
||||||
|
|
||||||
```json
|
- read-receipt `type=3/4` пока остаются в legacy-формате `SHiNE_dm2`
|
||||||
{
|
- контентные DM `type=1/2` используют `SHiNE_DM`
|
||||||
"op": "CallSignalToSession",
|
- сервер хранит только последнюю версию контентного сообщения по `messageKey`
|
||||||
"requestId": "call-signal-001",
|
- удаление сообщения реализуется новой ревизией с пустым телом и `attachmentsCount = 0`
|
||||||
"payload": {
|
- HTTP endpoints для DM-файлов сейчас отсутствуют
|
||||||
"toLogin": "bob",
|
|
||||||
"targetSessionId": "SESSION_ID",
|
|
||||||
"callId": "call-1",
|
|
||||||
"type": 101,
|
|
||||||
"data": "{\"sdp\":\"...\"}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Успешный ответ
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "CallSignalToSession",
|
|
||||||
"requestId": "call-signal-001",
|
|
||||||
"status": 200,
|
|
||||||
"ok": true,
|
|
||||||
"payload": {
|
|
||||||
"delivered": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Если целевая сессия не найдена или доставка не удалась, сервер может вернуть `404`.
|
|
||||||
|
|
||||||
## Типовые ошибки
|
|
||||||
|
|
||||||
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
|
|
||||||
- `400 / BAD_FIELDS` — не заполнены обязательные поля.
|
|
||||||
- `404 / USER_NOT_FOUND` — пользователь не найден.
|
|
||||||
- `404 / SESSION_NOT_FOUND` — сессия не найдена.
|
|
||||||
- `422 / BAD_SIGNATURE` — подпись DM не прошла проверку.
|
|
||||||
- `422 / BAD_DEVICE_KEY` — некорректный device key отправителя.
|
|
||||||
- `422 / BAD_TIME_WINDOW` — время подписанного сообщения вне допустимого окна.
|
|
||||||
- `422 / REPLAY` — повторное сообщение заблокировано.
|
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Временное Test API для бесплатной загрузки аватаров в Arweave
|
||||||
|
|
||||||
|
> Статус: **временное тестовое решение**.
|
||||||
|
> Все операции из этого файла начинаются с `Test...`, чтобы это было видно сразу и в коде, и в UI.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Этот временный API даёт пользователю ограниченную бесплатную загрузку маленьких аватаров в Arweave:
|
||||||
|
|
||||||
|
- загрузка идёт через **серверный Arweave-кошелёк**;
|
||||||
|
- лимит на пользователя: по умолчанию `3` загрузки за всё время;
|
||||||
|
- лимит хранится в SQLite-таблице `test_free_avatar_uploads`;
|
||||||
|
- если лимит исчерпан, сервер возвращает понятную ошибку;
|
||||||
|
- загружать можно только маленький итоговый файл аватара, по умолчанию до `128 KB`.
|
||||||
|
|
||||||
|
## Настройки сервера
|
||||||
|
|
||||||
|
В `application.properties`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
test.freeAvatar.enabled=true
|
||||||
|
test.freeAvatar.gateway=https://arweave.net
|
||||||
|
test.freeAvatar.limitPerUser=3
|
||||||
|
test.freeAvatar.maxBytes=131072
|
||||||
|
test.freeAvatar.walletAddress=
|
||||||
|
test.freeAvatar.walletJwkPath=
|
||||||
|
```
|
||||||
|
|
||||||
|
Пояснения:
|
||||||
|
|
||||||
|
- `test.freeAvatar.enabled` - включить или выключить временный API;
|
||||||
|
- `test.freeAvatar.gateway` - Arweave gateway для `price/tx/wallet`;
|
||||||
|
- `test.freeAvatar.limitPerUser` - пожизненный бесплатный лимит на пользователя;
|
||||||
|
- `test.freeAvatar.maxBytes` - максимальный размер итогового файла;
|
||||||
|
- `test.freeAvatar.walletAddress` - публичный адрес серверного Arweave-кошелька;
|
||||||
|
- `test.freeAvatar.walletJwkPath` - путь к приватному JWK-файлу серверного кошелька.
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- приватный JWK хранится вне кода;
|
||||||
|
- если `walletAddress` указан и не совпадает с адресом, вычисленным из JWK, сервер вернёт ошибку настройки.
|
||||||
|
|
||||||
|
## `TestGetFreeAvatarQuota`
|
||||||
|
|
||||||
|
Возвращает остаток бесплатных загрузок для текущего авторизованного пользователя.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestGetFreeAvatarQuota",
|
||||||
|
"requestId": "req-test-avatar-quota-1",
|
||||||
|
"payload": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestGetFreeAvatarQuota",
|
||||||
|
"requestId": "req-test-avatar-quota-1",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"enabled": true,
|
||||||
|
"limit": 3,
|
||||||
|
"usedCount": 1,
|
||||||
|
"remainingCount": 2,
|
||||||
|
"maxBytes": 131072
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `enabled` - временный API сейчас включён на сервере или нет;
|
||||||
|
- `limit` - полный лимит бесплатных загрузок;
|
||||||
|
- `usedCount` - сколько уже израсходовано;
|
||||||
|
- `remainingCount` - сколько ещё осталось;
|
||||||
|
- `maxBytes` - максимальный размер итогового файла.
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `422 NOT_AUTHENTICATED` - требуется авторизация.
|
||||||
|
|
||||||
|
## `TestUploadFreeAvatar`
|
||||||
|
|
||||||
|
Временная бесплатная загрузка маленькой аватарки в Arweave через серверный кошелёк.
|
||||||
|
|
||||||
|
### Правила
|
||||||
|
|
||||||
|
- операция требует авторизованную сессию;
|
||||||
|
- сервер использует текущий login из сессии;
|
||||||
|
- сервер принимает только:
|
||||||
|
- `image/jpeg`
|
||||||
|
- `image/png`
|
||||||
|
- `image/webp`
|
||||||
|
- размер итогового файла должен быть не больше `maxBytes` из квоты;
|
||||||
|
- если пользователь уже сделал `limit` бесплатных загрузок, операция запрещена.
|
||||||
|
|
||||||
|
### Запрос
|
||||||
|
|
||||||
|
`fileBytesBase64` - это обычный Base64 байт итогового подготовленного файла.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestUploadFreeAvatar",
|
||||||
|
"requestId": "req-test-avatar-upload-1",
|
||||||
|
"payload": {
|
||||||
|
"contentType": "image/webp",
|
||||||
|
"fileBytesBase64": "UklGRiQAAABXRUJQVlA4WAoAAAAQAAAA...",
|
||||||
|
"sha256Hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Успешный ответ
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "TestUploadFreeAvatar",
|
||||||
|
"requestId": "req-test-avatar-upload-1",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"txId": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
"sha256Hex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||||
|
"usedCount": 2,
|
||||||
|
"remainingCount": 1,
|
||||||
|
"limit": 3,
|
||||||
|
"gateway": "https://arweave.net"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Поля ответа
|
||||||
|
|
||||||
|
- `txId` - Arweave Transaction ID загруженного файла;
|
||||||
|
- `sha256Hex` - SHA-256 загруженного файла;
|
||||||
|
- `usedCount` - сколько бесплатных загрузок уже израсходовано после этой операции;
|
||||||
|
- `remainingCount` - сколько бесплатных загрузок осталось;
|
||||||
|
- `limit` - общий лимит;
|
||||||
|
- `gateway` - gateway, через который сервер отправлял транзакцию.
|
||||||
|
|
||||||
|
### Ошибки
|
||||||
|
|
||||||
|
- `422 NOT_AUTHENTICATED` - требуется авторизация;
|
||||||
|
- `400 BAD_FIELDS` - не переданы `contentType` или `fileBytesBase64`;
|
||||||
|
- `400 BAD_BASE64` - `fileBytesBase64` не декодируется;
|
||||||
|
- `400 BAD_AVATAR_FILE` - файл не проходит ограничения сервера;
|
||||||
|
- `400 FREE_AVATAR_LIMIT_EXHAUSTED` - бесплатный лимит аватарок исчерпан;
|
||||||
|
- `501 FREE_AVATAR_TEMP_DISABLED` - временная функция выключена или сервер не настроен;
|
||||||
|
- `500 INTERNAL_ERROR` - внутренняя ошибка сервера.
|
||||||
|
|
||||||
|
## Как это используется в UI
|
||||||
|
|
||||||
|
На экране редактирования профиля в мастере смены аватара есть временный сценарий:
|
||||||
|
|
||||||
|
- `Залить аватар бесплатно`
|
||||||
|
|
||||||
|
UI:
|
||||||
|
|
||||||
|
1. вызывает `TestGetFreeAvatarQuota`;
|
||||||
|
2. показывает остаток лимита;
|
||||||
|
3. локально подготавливает уменьшенный файл аватара;
|
||||||
|
4. проверяет, что итоговый файл не превышает `maxBytes`;
|
||||||
|
5. вызывает `TestUploadFreeAvatar`;
|
||||||
|
6. после получения `txId` обычным путём записывает `avatar.ar` в профиль через `AddBlock`.
|
||||||
|
|
||||||
|
## Почему решение временное
|
||||||
|
|
||||||
|
- используется общий серверный Arweave-кошелёк;
|
||||||
|
- лимит хранится отдельной технической таблицей;
|
||||||
|
- операции имеют префикс `Test...`;
|
||||||
|
- сценарий нужен как переходный бесплатный путь для маленьких аватаров.
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
|
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
|
||||||
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
|
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
|
||||||
- `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
|
- `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
|
||||||
- `medium/2026-06-02_сессионные_саб_серверы_в_pda.md` - несколько саб-серверов пользователя как типизированные сессии в PDA с версией записи.
|
- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи.
|
||||||
|
|
||||||
### DAO-запуск
|
### DAO-запуск
|
||||||
|
|
||||||
@@ -45,4 +45,4 @@
|
|||||||
|
|
||||||
### Дальнее будущее
|
### Дальнее будущее
|
||||||
|
|
||||||
- Сейчас задач нет.
|
- `far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md` - технические команды для homeserver через SHiNE/WebRTC DataChannel и обмен файлами по чанкам с адресацией по `SHA-256`.
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
# Homeserver: технические команды и передача файлов через SHiNE/WebRTC
|
||||||
|
|
||||||
|
## Зачем нужна фича
|
||||||
|
|
||||||
|
Идея на дальнее будущее: дать возможность обращаться к homeserver не только как к участнику сети SHiNE, но и как к удалённой технической точке управления.
|
||||||
|
|
||||||
|
Цели:
|
||||||
|
- отправлять на homeserver технические команды в текстовом виде;
|
||||||
|
- получать текстовый ответ на команду;
|
||||||
|
- при наличии WebRTC DataChannel передавать части файлов в обе стороны;
|
||||||
|
- хранить полученные файлы на SD-карте homeserver;
|
||||||
|
- использовать единый механизм доставки как через сервер SHiNE, так и напрямую через DataChannel.
|
||||||
|
|
||||||
|
## Горизонт
|
||||||
|
|
||||||
|
`far` - идея без ближайшего срока реализации. Сейчас приоритет ниже, чем запуск и стабилизация основного проекта.
|
||||||
|
|
||||||
|
## Что именно имеется в виду
|
||||||
|
|
||||||
|
### 1. Единая модель технической команды
|
||||||
|
|
||||||
|
Техническая команда должна иметь единый смысл независимо от транспорта доставки:
|
||||||
|
- через любой доступный сервер SHiNE;
|
||||||
|
- через уже установленный WebRTC DataChannel.
|
||||||
|
|
||||||
|
Если конкретный транспорт недоступен, ответ по нему может не прийти. Это считается нормальным поведением протокола.
|
||||||
|
|
||||||
|
### 2. Команда как короткоживущий подписанный сигнал
|
||||||
|
|
||||||
|
У команды должны быть:
|
||||||
|
- `commandId`;
|
||||||
|
- временная метка;
|
||||||
|
- TTL около 10 секунд;
|
||||||
|
- криптографическая подпись.
|
||||||
|
|
||||||
|
Смысл такой:
|
||||||
|
- если команда быстро дошла, homeserver подтверждает принятие;
|
||||||
|
- если не дошла вовремя, команда считается протухшей;
|
||||||
|
- отправитель может безопасно послать повтор;
|
||||||
|
- при повторе homeserver отвечает либо `команда принята`, либо `уже выполнено ранее`.
|
||||||
|
|
||||||
|
Это даёт дедупликацию и безопасный resend без повторного выполнения действия.
|
||||||
|
|
||||||
|
### 3. Текстовые технические команды
|
||||||
|
|
||||||
|
Базовый сценарий похож на короткий удалённый shell-протокол, но на уровне строго ограниченных команд:
|
||||||
|
- отправил строку-команду;
|
||||||
|
- получил строку-ответ.
|
||||||
|
|
||||||
|
Команды не обязаны исполнять произвольный shell. Предпочтительная модель - белый список операций с контролируемым форматом аргументов и ответа.
|
||||||
|
|
||||||
|
### 4. Передача файлов только при наличии DataChannel
|
||||||
|
|
||||||
|
Если между устройствами есть WebRTC DataChannel, через него можно передавать технические сообщения для файлового обмена.
|
||||||
|
|
||||||
|
Предварительная модель:
|
||||||
|
- имя файла = `SHA-256` содержимого;
|
||||||
|
- можно запросить диапазон байт `from..to`;
|
||||||
|
- можно отправить диапазон байт `from..to`;
|
||||||
|
- homeserver хранит полученные данные на SD-карте;
|
||||||
|
- если DataChannel нет, на запрос файловой передачи возвращается ответ в духе `не могу передать, нет data channel`.
|
||||||
|
|
||||||
|
Фактически файл-обмен должен быть частным случаем общего протокола технических команд.
|
||||||
|
|
||||||
|
### 5. Установка data-соединения по явной команде
|
||||||
|
|
||||||
|
Нужна техническая команда уровня:
|
||||||
|
- `установить data-соединение`.
|
||||||
|
|
||||||
|
Ответ:
|
||||||
|
- либо `да`, после чего запускается обычная процедура `offer/answer/ICE`;
|
||||||
|
- либо `нет` и причина отказа.
|
||||||
|
|
||||||
|
### 6. Доставка на пользовательские сессии
|
||||||
|
|
||||||
|
Логика должна быть совместима с общей моделью SHiNE, где технические сигналы можно отправлять на конкретные активные сессии пользователя.
|
||||||
|
|
||||||
|
Идея:
|
||||||
|
- на любую активную сессию пользователя можно посылать техническую команду;
|
||||||
|
- контакт пользователя может инициировать такую техническую коммуникацию так же, как он уже инициирует звонок или другой служебный сигнал.
|
||||||
|
|
||||||
|
## Что нужно будет сделать при возврате к задаче
|
||||||
|
|
||||||
|
- Спроектировать отдельный формат технических команд и ack-ответов.
|
||||||
|
- Решить, будет ли это новый тип служебных сообщений в существующем протоколе блокчейн/сигналинга или отдельная ветка поверх уже имеющихся transport-операций.
|
||||||
|
- Отдельно продумать авторизацию: кто именно из контактов и какие команды имеет право слать.
|
||||||
|
- Ограничить набор допустимых команд, чтобы не превратить механизм в небезопасный удалённый shell.
|
||||||
|
- Спроектировать протокол чанков файлов: размер чанка, нумерация, повторная отправка, контроль целостности, дозагрузка, завершение файла.
|
||||||
|
- Продумать хранение на SD-карте: временные файлы, сборка чанков, проверка итогового `SHA-256`, очистка мусора.
|
||||||
|
- Продумать поведение при отсутствии DataChannel, таймаутах и дублирующихся командах.
|
||||||
|
- Проверить, как это лучше встраивать в текущие клиентские сессии, звонки и homeserver-логику.
|
||||||
|
|
||||||
|
## Вопросы для будущего уточнения
|
||||||
|
|
||||||
|
- Это должен быть строго служебный протокол или пользователь сможет вызывать его и вручную из UI.
|
||||||
|
- Нужен ли доступ только к заранее разрешённым каталогам/файлам.
|
||||||
|
- Нужна ли двусторонняя синхронизация файлов или достаточно ручных команд `запросить кусок` / `отправить кусок`.
|
||||||
|
- Нужно ли разрешать передачу файлов через сервер SHiNE как fallback, или файл-обмен должен идти только через DataChannel.
|
||||||
|
- Какой максимальный размер файлов и допустимый объём хранения на SD-карте.
|
||||||
|
|
||||||
|
## Что уже сделано
|
||||||
|
|
||||||
|
Пока только зафиксирована идея и базовая концепция. Реализация не начиналась.
|
||||||
|
|
||||||
|
## Какие документы нужно будет обновить при реализации
|
||||||
|
|
||||||
|
- `Dev_Docs/Blockchain/README.md` и связанные файлы, если изменятся типы служебных сообщений или форматы блокчейн-команд.
|
||||||
|
- `Dev_Docs/API/` если изменится публичный серверный API или появятся новые операции.
|
||||||
|
- `Dev_Docs/Personal_Messages/README.md` если часть маршрутизации или подтверждений будет встроена в существующую логику доставки/сессий.
|
||||||
|
- Документацию по homeserver/ESP32, если появится пользовательская или сервисная файловая логика на устройстве.
|
||||||
|
|
||||||
|
## С какого места продолжать позже
|
||||||
|
|
||||||
|
Возвращаться к задаче только после стабилизации запуска проекта и базовых текущих функций. Начинать с проектирования протокола команд и матрицы прав доступа, а уже потом переходить к DataChannel-файлообмену.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Дальнее будущее
|
# Дальнее будущее
|
||||||
|
|
||||||
Сейчас в этом горизонте нет активных идей.
|
|
||||||
|
|
||||||
Сюда переносить задачи, у которых нет понятного срока возврата и которые не нужно учитывать в ближайшем или среднесрочном планировании.
|
Сюда переносить задачи, у которых нет понятного срока возврата и которые не нужно учитывать в ближайшем или среднесрочном планировании.
|
||||||
|
|
||||||
|
## Идеи
|
||||||
|
|
||||||
|
- `2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md` - технические команды для homeserver через сервер SHiNE или WebRTC DataChannel, а также файловый обмен чанками с хранением на SD-карте.
|
||||||
|
|||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
# Сессионные саб-серверы в PDA пользователя
|
# Сессионные homeserver-ы в PDA пользователя
|
||||||
|
|
||||||
- Статус:
|
- Статус:
|
||||||
`future`
|
`future`
|
||||||
@@ -10,15 +10,15 @@
|
|||||||
после завершения первого этапа по пользовательским сессиям
|
после завершения первого этапа по пользовательским сессиям
|
||||||
|
|
||||||
- Основание:
|
- Основание:
|
||||||
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних саб-серверов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
|
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних homeserver-ов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
|
||||||
|
|
||||||
## Зачем нужна фича
|
## Зачем нужна фича
|
||||||
|
|
||||||
У одного пользователя может быть несколько доверенных внутренних саб-серверов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
|
У одного пользователя может быть несколько доверенных внутренних homeserver-ов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
|
||||||
|
|
||||||
Это нужно, чтобы:
|
Это нужно, чтобы:
|
||||||
|
|
||||||
- хранить несколько саб-серверов у одного пользователя одновременно;
|
- хранить несколько homeserver-ов у одного пользователя одновременно;
|
||||||
- различать обычные клиентские сессии и серверные сессии по явному типу;
|
- различать обычные клиентские сессии и серверные сессии по явному типу;
|
||||||
- дать расширяемый формат записи с версией;
|
- дать расширяемый формат записи с версией;
|
||||||
- использовать единый подход для DM, звонков и внутренних команд между сессиями.
|
- использовать единый подход для DM, звонков и внутренних команд между сессиями.
|
||||||
@@ -35,18 +35,18 @@
|
|||||||
Предварительные значения:
|
Предварительные значения:
|
||||||
|
|
||||||
- тип `1` - обычная пользовательская сессия;
|
- тип `1` - обычная пользовательская сессия;
|
||||||
- тип `100` - саб-сервер пользователя;
|
- тип `100` - homeserver пользователя;
|
||||||
- версия `1` - первая рабочая версия формата записи сессии.
|
- версия `1` - первая рабочая версия формата записи сессии.
|
||||||
|
|
||||||
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
|
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
|
||||||
|
|
||||||
Важно: саб-серверов у одного пользователя может быть несколько.
|
Важно: homeserver-ов у одного пользователя может быть несколько.
|
||||||
|
|
||||||
## Архитектурный принцип
|
## Архитектурный принцип
|
||||||
|
|
||||||
Внутренний протокол взаимодействия должен оставаться транспортным.
|
Внутренний протокол взаимодействия должен оставаться транспортным.
|
||||||
|
|
||||||
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки саб-сервера, а должен:
|
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки homeserver-а, а должен:
|
||||||
|
|
||||||
- доставлять сообщения между сессиями;
|
- доставлять сообщения между сессиями;
|
||||||
- доставлять сигналы звонков между сессиями;
|
- доставлять сигналы звонков между сессиями;
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
- Вызов звонка уже рассылается по нескольким активным сессиям пользователя.
|
- Вызов звонка уже рассылается по нескольким активным сессиям пользователя.
|
||||||
- Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя.
|
- Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя.
|
||||||
|
|
||||||
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол саб-сервера".
|
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол homeserver-а".
|
||||||
|
|
||||||
## Что нужно сделать при возврате к задаче
|
## Что нужно сделать при возврате к задаче
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
- правила удаления и обновления записи;
|
- правила удаления и обновления записи;
|
||||||
- правила ротации `sessionPubKey`.
|
- правила ротации `sessionPubKey`.
|
||||||
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
|
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
|
||||||
7. Определить, какие внутренние сообщения саб-сервера останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
7. Определить, какие внутренние сообщения homeserver-а останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
||||||
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
||||||
9. После реализации обязательно обновить документацию.
|
9. После реализации обязательно обновить документацию.
|
||||||
|
|
||||||
@@ -101,5 +101,5 @@
|
|||||||
Продолжать после завершения первой части:
|
Продолжать после завершения первой части:
|
||||||
|
|
||||||
1. описать минимальный формат записи пользовательской сессии;
|
1. описать минимальный формат записи пользовательской сессии;
|
||||||
2. отдельно решить, живут ли саб-серверы в том же списке, что и обычные сессии;
|
2. отдельно решить, живут ли homeserver-ы в том же списке, что и обычные сессии;
|
||||||
3. затем уже проектировать операции регистрации, обновления и отключения таких сессий.
|
3. затем уже проектировать операции регистрации, обновления и отключения таких сессий.
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Деривация секрета и ключей SHiNE (формулы)
|
||||||
|
|
||||||
|
> **Статус: ИСТОЧНИК ИСТИНЫ (single source of truth) по конкретной деривации.**
|
||||||
|
> Этот файл описывает, как из пароля получается секрет и как из секрета выводятся
|
||||||
|
> все ключи (root, blockchain, device/Solana, homeserver) — формулами, байт-в-байт.
|
||||||
|
> Если в коде меняется деривация (формула секрета, параметры Argon2id, соль, формула
|
||||||
|
> ключа, разделитель `|`, набор/имена суффиксов, формат homeserver-ключа, связь
|
||||||
|
> dev-ключ ↔ Solana-адрес) — **в том же изменении обязательно править этот документ**.
|
||||||
|
> Роли и назначение ключей описаны отдельно в `Dev_Docs/Keys/README.md` (архитектура).
|
||||||
|
> Здесь — только механика. Документ намеренно краткий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Секрет (masterSecret)
|
||||||
|
|
||||||
|
`masterSecret` — 32 байта. Два источника:
|
||||||
|
|
||||||
|
**А. Из пароля пользователя (основной путь, UI).**
|
||||||
|
|
||||||
|
```
|
||||||
|
login = trim(lowercase(login))
|
||||||
|
salt = SHA-256("shine-auth-v2|login=" + login + "|suffix=master.secret")[0..16) // первые 16 байт
|
||||||
|
material = utf8(login + "\n" + password)
|
||||||
|
masterSecret(32) = Argon2id(material, salt, t=2, m=65536 KiB, p=1, dkLen=32)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Параметры Argon2id фиксированы: `t=2`, `m=65536` (64 МиБ), `p=1`, `dkLen=32`.
|
||||||
|
- Логин входит и в соль, и в начало `material` (склейка через `\n`).
|
||||||
|
- Пустой пароль **запрещён**: легаси-fallback без Argon2 удалён, `deriveMasterSecretFromPassword` бросает ошибку на пустом пароле, а форма регистрации в UI блокирует пустой пароль (`register-view.js`).
|
||||||
|
|
||||||
|
**Б. Случайный (прошивка ESP32, новый аккаунт без пароля).**
|
||||||
|
|
||||||
|
```
|
||||||
|
masterSecret(32) = 32 случайных байта (esp_random) // хранится на устройстве как base58
|
||||||
|
```
|
||||||
|
|
||||||
|
Дальше деривация ключей одинакова независимо от источника секрета.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Производные ключи
|
||||||
|
|
||||||
|
Все ключи выводятся из `masterSecret` по **одной формуле**, отличается только суффикс:
|
||||||
|
|
||||||
|
```
|
||||||
|
material = base64_std(masterSecret) + "|" + <суффикс>
|
||||||
|
seed(32) = SHA-256(material)
|
||||||
|
(pub, priv) = Ed25519_keypair_from_seed(seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `base64_std` — стандартный base64 (не url-safe).
|
||||||
|
- Разделитель — символ `|`.
|
||||||
|
- Суффиксы значимы байт-в-байт (регистр и точки важны).
|
||||||
|
|
||||||
|
| Ключ | Суффикс | Назначение (кратко) |
|
||||||
|
|------|---------|---------------------|
|
||||||
|
| root | `root.key` | Личность. Подписывает unsigned-часть PDA-записи (`RootKeyBlock`). |
|
||||||
|
| blockchain | `bch.key` | Подписывает `LastBlockState` персонального блокчейна (`blockchain_public_key`). |
|
||||||
|
| device / **Solana** | `dev.key` | Ключ устройства = Solana-ключ. Fee payer и подпись Solana-транзакций; адрес кошелька = `base58(devicePub)`. См. §3. |
|
||||||
|
| homeserver | `homeserver.key:<имя>` | Ключ homeserver-устройства, по одному на каждый homeserver (различитель — имя). См. §4. |
|
||||||
|
|
||||||
|
Полные роли каждого ключа — в `Dev_Docs/Keys/README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Solana-ключ
|
||||||
|
|
||||||
|
Отдельного «солана-ключа» нет. На Solana работают два ключа:
|
||||||
|
|
||||||
|
- **`dev.key` (device) — пополняемый кошелёк и fee payer.** Solana-адрес = `base58(devicePub)`.
|
||||||
|
Этим ключом оплачиваются и подписываются `create_user_pda` / `update_user_pda`.
|
||||||
|
Пополнять SOL нужно именно на этот адрес.
|
||||||
|
- **`root.key` — авторитет записи**, подписывает unsigned-часть PDA через Ed25519-инструкцию, но **не** является fee payer.
|
||||||
|
|
||||||
|
Соответствует формату PDA `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` §2.1
|
||||||
|
(«create/update оплачиваются с `device_key`», «root_key — не fee payer»).
|
||||||
|
|
||||||
|
Кратко про роли на Solana: `root.key` — это **главный (master) ключ**: им управляют PDA-записью
|
||||||
|
(`create/update`) и через это можно заменить все остальные ключи; `dev.key` — это **пополняемый
|
||||||
|
кошелёк и плательщик комиссий**. Полное описание ролей — `Dev_Docs/Keys/README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ключи homeserver
|
||||||
|
|
||||||
|
У пользователя может быть несколько homeserver-ов. Каждый имеет **своё имя** и **свой приватный ключ**,
|
||||||
|
выведенный из секрета по той же формуле с именованным суффиксом:
|
||||||
|
|
||||||
|
```
|
||||||
|
suffix = "homeserver.key:" + <имя homeserver> // имя по умолчанию: "homeserver1"
|
||||||
|
material = base64_std(masterSecret) + "|" + suffix
|
||||||
|
seed(32) = SHA-256(material)
|
||||||
|
(pub, priv) = Ed25519_keypair_from_seed(seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример для двух homeserver-ов:
|
||||||
|
|
||||||
|
```
|
||||||
|
homeserver.key:home-a -> ключ A
|
||||||
|
homeserver.key:home-b -> ключ B
|
||||||
|
```
|
||||||
|
|
||||||
|
Публичный ключ homeserver-а публикуется в `SessionsBlock` пользовательской PDA как
|
||||||
|
`session_pub_key` с `session_type = 100`, имя — в `session_name` (формат PDA §13).
|
||||||
|
|
||||||
|
> Это переименование прежней схемы `subserver.key:<имя>` → `homeserver.key:<имя>`.
|
||||||
|
> Термин «саб-сервер» по проекту заменяется на «homeserver».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Где это в коде
|
||||||
|
|
||||||
|
### Деривация секрета и ключей (UI, каноническая)
|
||||||
|
- `shine-UI/js/services/crypto-utils.js`
|
||||||
|
- секрет из пароля: `makeArgon2Salt`, `deriveMasterSecretArgon2id`, `deriveMasterSecretFromPassword` (~129–218);
|
||||||
|
- ключ из секрета: `deriveEd25519FromMasterSecret` (~220).
|
||||||
|
- `shine-UI/js/services/auth-service.js` — набор root/bch/dev из `masterSecret` (~732–758).
|
||||||
|
- `shine-UI/server-ui/js/server-ui-shared.js` — те же root/bch/dev для серверного UI (~147–160).
|
||||||
|
|
||||||
|
### Solana-ключ / адрес кошелька (UI)
|
||||||
|
- `shine-UI/js/pages/registration-payment-view.js` — `deriveUserWalletAddress`: адрес = `base58(devicePub)` (~113).
|
||||||
|
- `shine-UI/js/pages/topup-view.js` — `deviceWalletAddressFromBundle`: тот же канонический адрес из `preGeneratedKeyBundle.devicePair`.
|
||||||
|
Прежний расходящийся путь `deriveWalletFromPassword` (прямой Argon2 по `dev.key`, мимо `masterSecret`) удалён.
|
||||||
|
|
||||||
|
### Деривация ключей (прошивка ESP32)
|
||||||
|
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino`
|
||||||
|
- основной скетч ESP32-проекта `SHiNE`; `deriveKeysFromMasterSecret` (~782), `restoreDerivedKeysFromSecret` (~806), `deriveFreshSecretAndWallet` (~829);
|
||||||
|
- регистрация/подпись Solana: `registerHomeserverOnSolana` (~1182), `signMessageEd25519` (~1147).
|
||||||
|
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_ui/shine_homeserver_ui.ino`
|
||||||
|
- старый тестовый вариант; оставлен как legacy-скетч для сравнения и диагностики.
|
||||||
|
|
||||||
|
### Формат PDA (куда попадают ключи)
|
||||||
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||||
|
— `RootKeyBlock` §6, `DeviceKeyBlock` §7, `blockchain_public_key` §9, `SessionsBlock`/`session_type=100` §13, оплата §2.1.
|
||||||
|
|
||||||
|
### Сервер (тестовый seed)
|
||||||
|
- `SHiNE-server/src/test/java/test/it/cases/SeedDataPopulationHelper.java` `deriveKeysFromPassword` (~246) —
|
||||||
|
выводит ключи как `Ed25519(SHA-256(base64(SHA-256(password)) + suffix))`, **без** Argon2 и **без** разделителя `|`.
|
||||||
|
Это **не баг**, а точное повторение легаси-пути UI `derivePasswordSeed` (для пустого пароля), у которого тоже нет `|`.
|
||||||
|
С современным путём `masterSecret`-bundle (Argon2 + `base64(secret)|suffix`) он **не совпадает** by design.
|
||||||
|
Если потребуется, чтобы seed совпадал с реальными клиентами на Argon2 — нужно отдельно портировать
|
||||||
|
Argon2id+masterSecret в Java (на сервере Argon2 сейчас нет). Простое добавление `|` было бы **неверным**:
|
||||||
|
сломало бы совпадение с легаси-путём и всё равно не дало бы совпадения с Argon2-путём.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Правило синхронизации (обязательно)
|
||||||
|
|
||||||
|
1. Этот документ — источник истины по деривации секрета и ключей.
|
||||||
|
2. Любое изменение кода, затрагивающее формулу секрета, параметры Argon2id, соль, формулу ключа,
|
||||||
|
разделитель `|`, набор/имена суффиксов, формат homeserver-ключа или связь dev-ключ ↔ Solana-адрес —
|
||||||
|
**обязательно** отражать здесь в том же изменении.
|
||||||
|
3. Пункты, помеченные ⚠️, — это долг к устранению, а не норма.
|
||||||
|
4. Нельзя сознательно оставлять код и этот документ в рассинхроне без отдельной явной договорённости.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
В SHiNE у пользователя есть несколько уровней ключей:
|
В SHiNE у пользователя есть несколько уровней ключей:
|
||||||
|
|
||||||
- `root key` - главный корневой ключ пользователя, он же основной Solana-ключ.
|
- `root key` - главный (master) ключ пользователя: тот, кто им владеет, управляет пользовательской PDA в Solana и может заменить все остальные ключи. Это не пополняемый кошелёк (комиссии платит `device key`).
|
||||||
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
- `blockchain key` - ключ записи в персональный SHiNE-блокчейн пользователя.
|
||||||
- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
|
- `device key` - общий ключ пользовательских устройств для повседневной работы, звонков, DM и мелких платежей.
|
||||||
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
|
- `session key` - ключ конкретной сессии или конкретного устройства для авторизации на сервере.
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
- управление остальными ключами;
|
- управление остальными ключами;
|
||||||
- подтверждение операций, которые должны иметь максимальный уровень доверия.
|
- подтверждение операций, которые должны иметь максимальный уровень доверия.
|
||||||
|
|
||||||
В текущей модели `root key` совпадает по смыслу с главным Solana-ключом пользователя.
|
`root key` — это **главный (master) ключ** в следующем смысле: зная `root key`, можно управлять пользовательской PDA-записью в Solana (`create_user_pda` / `update_user_pda`) и тем самым **заменить все остальные ключи** пользователя (device, blockchain, homeserver). Поэтому компрометация `root key` равносильна компрометации всей личности пользователя.
|
||||||
|
|
||||||
На `root key` могут храниться значимые средства, если пользователь сознательно выбирает такую модель. Для мелких текущих расходов предпочтительнее использовать `device key`.
|
Важно не путать авторитет и кошелёк: `root key` — это авторитет над PDA-записью, а **SOL-комиссии за create/update платит `device key`** (он же fee payer и адрес для пополнения). Подробнее о том, какой ключ за что отвечает на Solana, — в `Dev_Docs/Keys/DERIVATION.md`, §3.
|
||||||
|
|
||||||
## `blockchain key`
|
## `blockchain key`
|
||||||
|
|
||||||
@@ -158,6 +158,7 @@ Self-message - это сообщение пользователя самому
|
|||||||
|
|
||||||
## Связанные документы
|
## Связанные документы
|
||||||
|
|
||||||
|
- `Dev_Docs/Keys/DERIVATION.md` - **источник истины по конкретной деривации** секрета и ключей (формулы Argon2id, `base64|suffix→SHA-256→Ed25519`, суффиксы `root.key`/`bch.key`/`dev.key`/`homeserver.key:<имя>`, Solana-ключ, ссылки на код).
|
||||||
- `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений.
|
- `Dev_Docs/Personal_Messages/README.md` - текущая документация личных сообщений.
|
||||||
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
|
- `Dev_Docs/Blockchain/README.md` - точка входа по форматам SHiNE-блокчейна.
|
||||||
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
|
- `Dev_Docs/Solana_Architecture/README.md` - архитектура Solana-программ, PDA-счетов, DAO и движения средств.
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
# Кошелёк: лимит/закрепление блокчейна Сияния
|
|
||||||
|
|
||||||
- статус: `pending`
|
|
||||||
|
|
||||||
## Кратко что сделано
|
|
||||||
|
|
||||||
- На экране `Кошелёк -> Блокчейн Сияния` добавлены 2 слоя данных:
|
|
||||||
- фактическое состояние цепочки на сервере (`кол-во блоков`, `размер`, `крайний блок`, `hash`, `размер крайнего блока`);
|
|
||||||
- закреплённое состояние в Solana PDA (`лимит`, `использовано`, `остаток`, `крайний блок`, `hash`).
|
|
||||||
- Добавлены действия:
|
|
||||||
- `Закрепить в Solana` — обновляет PDA до текущего состояния серверной цепочки;
|
|
||||||
- `Увеличить лимит` — увеличивает `paid_limit_bytes` в PDA с учётом цены из economy PDA.
|
|
||||||
- Если `rootKey`/`blockchainKey` не сохранены локально, экран запрашивает пароль, восстанавливает ключи через стандартную derivation-логику и предлагает сохранить их в зашифрованный контейнер.
|
|
||||||
|
|
||||||
## Что проверять вручную
|
|
||||||
|
|
||||||
1. Открыть `Кошелёк -> Блокчейн Сияния` под авторизованным пользователем.
|
|
||||||
2. Проверить, что в блоке "Фактическое состояние на сервере" отображаются:
|
|
||||||
- число блоков;
|
|
||||||
- размер цепочки;
|
|
||||||
- номер/хэш крайнего блока;
|
|
||||||
- размер крайнего блока.
|
|
||||||
3. Проверить, что в блоке "Закреплено в Solana" отображаются:
|
|
||||||
- лимит;
|
|
||||||
- израсходовано;
|
|
||||||
- остаток;
|
|
||||||
- номер/хэш крайнего закреплённого блока.
|
|
||||||
4. Нажать `Закрепить в Solana` и убедиться, что:
|
|
||||||
- приходит успешная транзакция;
|
|
||||||
- после обновления Solana-показатели подтягиваются до серверных (или максимально близко по актуальному состоянию).
|
|
||||||
5. Нажать `Увеличить лимит`, ввести значение кратное шагу, подтвердить списание и проверить:
|
|
||||||
- лимит увеличился;
|
|
||||||
- отображение цены/списания соответствует economy PDA.
|
|
||||||
6. Повторить пункты 4-5 в сценарии, когда `rootKey`/`blockchainKey` не сохранены, и проверить:
|
|
||||||
- появляется запрос пароля;
|
|
||||||
- после ввода пароля операции выполняются;
|
|
||||||
- предложение сохранить ключи показывается.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
- Экран корректно разделяет "фактическое состояние на сервере" и "закреплённое в Solana".
|
|
||||||
- Обе операции (`Закрепить в Solana`, `Увеличить лимит`) выполняются без ошибок при валидных данных.
|
|
||||||
- Восстановление ключей через пароль работает, а без нужных ключей операция не выполняется молча.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Озвучивание ответов агента
|
|
||||||
|
|
||||||
## Что сделано
|
|
||||||
|
|
||||||
В локальный Telegram-бот-сервис агента-кодера добавлены персональные настройки озвучивания финальных ответов:
|
|
||||||
|
|
||||||
- `/voice_on` включает озвучивание для текущего Telegram-пользователя;
|
|
||||||
- `/voice_off` выключает озвучивание для текущего Telegram-пользователя;
|
|
||||||
- `/voice_status` показывает текущее состояние;
|
|
||||||
- если озвучивание включено, после текстового финального ответа сервис генерирует voice-файл через OpenAI TTS и отправляет его в Telegram;
|
|
||||||
- длинные ответы делятся на несколько фрагментов озвучки.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
|
|
||||||
1. Перезапустить `shine-agent-bot-coder`.
|
|
||||||
2. Отправить `/voice_status` и убедиться, что по умолчанию озвучивание выключено.
|
|
||||||
3. Отправить `/voice_on`.
|
|
||||||
4. Дать простую задачу агенту и проверить, что пришёл полный текстовый ответ и voice-файл с тем же ответом.
|
|
||||||
5. Отправить `/voice_off`.
|
|
||||||
6. Дать ещё одну простую задачу и проверить, что приходит только текст.
|
|
||||||
7. При возможности проверить второго whitelist-пользователя: его настройка должна быть независимой.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
Настройка хранится персонально по username и сохраняется после перезапуска сервиса. При включённой настройке Telegram получает текстовый ответ и дополнительное voice-сообщение с озвучкой. При выключенной настройке поведение остаётся прежним.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
pending
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Голосовая адаптация ответов Telegram-бота
|
|
||||||
|
|
||||||
## Краткое описание
|
|
||||||
Добавлены персональные настройки голосовых ответов и адаптации текста перед озвучкой. Если голосовые ответы включены, сервис перед TTS может отдельно прогонять финальный текст через OpenAI-модель и отправлять более короткую голосовую версию в исходный чат, личный чат пользователя и общий чат `@shine_writing`, если эти чаты доступны и отличаются.
|
|
||||||
|
|
||||||
## Что проверить
|
|
||||||
- Команды `/voice_on`, `/voice_off`, `/voice_status` для конкретного пользователя.
|
|
||||||
- Команды `/voice_rewrite_on`, `/voice_rewrite_off`, `/voice_rewrite_status` для конкретного пользователя.
|
|
||||||
- Команда `/status` показывает очередь, голосовые ответы и адаптацию текста перед озвучкой.
|
|
||||||
- При включённых голосовых ответах после задачи приходит текстовый ответ и voice-ответ.
|
|
||||||
- При включённой адаптации voice-ответ короче и без длинных технических строк.
|
|
||||||
- При задаче из личного чата voice дополнительно появляется в общем чате `@shine_writing`.
|
|
||||||
- При задаче из общего чата voice дополнительно появляется в личном чате пользователя, если сервис уже знает его личный chat_id.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
Текстовый ответ остаётся полным. Голосовая версия приходит отдельно, звучит короче и естественнее, а персональные настройки одного пользователя не меняют поведение других пользователей.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
pending
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Эксперимент Understand Anything
|
|
||||||
|
|
||||||
## Краткое описание
|
|
||||||
|
|
||||||
Добавлена изолированная лаборатория для проверки `Lum1104/Understand-Anything` без подключения к сборке, деплою и рабочему коду SHiNE.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
|
|
||||||
- Установить Node.js 22+ и pnpm 10+.
|
|
||||||
- Запустить `./tools/understand-anything-lab/install_codex_skills.sh`.
|
|
||||||
- Перезапустить Codex-сессию.
|
|
||||||
- Выполнить `/understand --language ru` в корне проекта.
|
|
||||||
- После генерации выполнить `/understand-dashboard` и проверить, что граф открывается и помогает ориентироваться по серверным, UI, Solana и агентским папкам.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
- В проекте появляется локальная папка `.understand-anything/` с графом знаний.
|
|
||||||
- Dashboard открывается и показывает интерактивный граф проекта.
|
|
||||||
- Основные процессы сборки и деплоя SHiNE не меняются.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
`pending`
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Центр задач Telegram-агента
|
|
||||||
|
|
||||||
## Краткое описание
|
|
||||||
|
|
||||||
Добавлена первая версия центра задач и предложений внутри `SHiNE-agent-bot-coder`.
|
|
||||||
|
|
||||||
Бот хранит задачи и предложения в JSON-файле данных сервиса, умеет показывать список через `/tasks`, создавать задачи для игроков по фразе Айдара, принимать предложения игроков по префиксу `предложение:`, менять статусы и добавлять короткие напоминания после ответов.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
|
|
||||||
- Айдар пишет `/tasks` и видит текущий список задач и предложений без уже закрытого предложения от Димы.
|
|
||||||
- Айдар пишет `поставь задачу Милане: проверить описание SHiNE` и задача появляется в списке Миланы.
|
|
||||||
- Милана пишет `/tasks` и видит назначенную задачу.
|
|
||||||
- Игрок пишет `предложение: ...`, после чего предложение появляется у Айдара.
|
|
||||||
- Айдар меняет статус фразами вида `одобрить TC-XXXX`, `доработать TC-XXXX`, `закрыть TC-XXXX`, где `TC-XXXX` - ID существующей задачи или предложения.
|
|
||||||
- После обычного ответа бота Айдару или игроку появляется короткое напоминание, если у пользователя есть активные задачи.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
Задачи и предложения сохраняются между перезапусками сервиса, статусы меняются корректно, напоминания не мешают основному ответу Codex.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
pending
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Рестарты и voice-настройки Telegram-агента
|
|
||||||
|
|
||||||
## Краткое описание
|
|
||||||
|
|
||||||
Добавлена первая версия безопасного рестарта Telegram-агента:
|
|
||||||
|
|
||||||
- `/restart` и `/restart_service` ставят отложенный рестарт после текущей задачи и до взятия следующей;
|
|
||||||
- `/restart_hard`, `/restart_now`, `/restart_force` выполняют жёсткий рестарт сразу;
|
|
||||||
- команды рестарта доступны только Айдару;
|
|
||||||
- voice-ответы включены по умолчанию для новых пользователей;
|
|
||||||
- адаптация текста перед озвучкой стала ближе к исходному ответу и не должна менять смысл;
|
|
||||||
- скрыты отдельные команды статуса voice-функций из справки, состояние показывается через `/status`.
|
|
||||||
|
|
||||||
## Что проверить
|
|
||||||
|
|
||||||
1. Отправить `/restart` во время активной задачи игрока или Айдара.
|
|
||||||
2. Убедиться, что активная задача завершается, после чего сервис перезапускается до следующей задачи.
|
|
||||||
3. Отправить `/restart_hard` и убедиться, что сервис перезапускается сразу.
|
|
||||||
4. Проверить, что игрок не может выполнить команды рестарта.
|
|
||||||
5. Проверить `/status`: он показывает очередь и состояния голосовых функций.
|
|
||||||
6. Проверить нового пользователя: voice-ответы должны быть включены по умолчанию.
|
|
||||||
7. Проверить текстовый запрос пользователя с включённым voice: после текстового ответа должен прийти voice-файл.
|
|
||||||
8. Проверить, что адаптированная озвучка не превращается в другой ответ, а только убирает длинные технические строки.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
Сервис можно обновлять без потери текущей задачи через отложенный рестарт. Жёсткий рестарт остаётся аварийной командой Айдара. Voice-ответы работают для текстовых и голосовых запросов, а голосовая версия остаётся близкой к текстовой.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
pending
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Кнопки вкладки «Каналы»
|
|
||||||
|
|
||||||
## Что сделано
|
|
||||||
|
|
||||||
Доработана верхняя панель вкладки «Каналы»:
|
|
||||||
- при открытии нижней кнопкой «Каналы» показывается режим «Все каналы»;
|
|
||||||
- в режиме «Все каналы» справа доступны кнопка «Мои каналы» и иконка поиска канала;
|
|
||||||
- в режиме «Мои каналы» доступен переход обратно во «Все каналы», а справа показывается плюсик создания канала.
|
|
||||||
|
|
||||||
## Что проверить
|
|
||||||
|
|
||||||
1. Открыть вкладку «Каналы» через нижнюю навигацию.
|
|
||||||
2. Убедиться, что открыт режим «Все каналы», а плюсик создания канала не отображается.
|
|
||||||
3. Нажать иконку поиска в режиме «Все каналы».
|
|
||||||
4. Убедиться, что открывается текущий сценарий поиска каналов.
|
|
||||||
5. Нажать «Мои каналы».
|
|
||||||
6. Убедиться, что справа появился плюсик создания канала.
|
|
||||||
7. Нажать «Все каналы» или стрелку назад и проверить возврат к режиму «Все каналы».
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
Кнопки верхней панели соответствуют активному режиму: поиск в «Все каналы», создание только в «Мои каналы».
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
pending
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Длинные voice/audio в Telegram-боте агента
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
Бот теперь умеет обрабатывать длинные voice/audio аккуратнее: учитывает лимит Telegram Bot API на скачивание слишком больших файлов, поддерживает альтернативный `TELEGRAM_API_BASE_URL` для локального `telegram-bot-api`, локально пережимает длинное аудио через `ffmpeg`, режет на куски и отправляет их в OpenAI transcription последовательно.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Короткий `voice` по-прежнему распознаётся без заметной задержки.
|
|
||||||
2. Длинный `audio/voice`, который помещается в скачивание Telegram, успешно пережимается, режется на части и даёт цельную расшифровку.
|
|
||||||
3. Очень большой файл через обычный `https://api.telegram.org` даёт понятное сообщение про лимит Telegram.
|
|
||||||
4. После переключения на локальный `telegram-bot-api` такой же большой файл начинает скачиваться и распознаваться.
|
|
||||||
- ожидаемый результат:
|
|
||||||
Бот не падает на длинных аудио, даёт либо расшифровку, либо понятное объяснение, какой именно лимит мешает и что нужно включить.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Диагностика больших voice/audio в Telegram-боте
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
- Бот при большом voice/audio больше не отказывается заранее по метаданным Telegram. Теперь он сначала сообщает, что пробует скачать файл, затем отдельно сообщает об успешном скачивании и только после этого переходит к подготовке аудио и распознаванию через OpenAI.
|
|
||||||
- что именно проверять:
|
|
||||||
- Отправить в бота большой `voice` или `audio`, который раньше попадал под ранний отказ.
|
|
||||||
- Проверить, что сначала приходит сообщение о попытке скачать большой файл.
|
|
||||||
- Проверить два сценария:
|
|
||||||
- скачивание удалось: бот пишет об успешной загрузке и продолжает распознавание;
|
|
||||||
- скачивание не удалось: бот пишет именно о неудачном скачивании из Telegram, без ложной привязки к ошибке OpenAI.
|
|
||||||
- ожидаемый результат:
|
|
||||||
- Пользователь видит понятную поэтапную диагностику: попытка скачивания, результат скачивания и только потом следующий этап обработки.
|
|
||||||
- статус:
|
|
||||||
- pending
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Перенос server UI в shine-UI
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
Веб-панель управления серверной Solana PDA перенесена в `shine-UI/` как отдельные страницы.
|
|
||||||
Новая точка входа: `shine-UI/server-ui.html`.
|
|
||||||
Общая логика работы с PDA вынесена в единый модуль `shine-UI/js/services/shine-user-pda-service.js`.
|
|
||||||
|
|
||||||
- что именно проверять:
|
|
||||||
1. Открытие `shine-UI/server-ui.html` и переходы на страницы создания и обновления PDA.
|
|
||||||
2. Генерацию ключей из логина и пароля на странице создания.
|
|
||||||
3. Ручной ввод base58-ключей и регистрацию серверного PDA.
|
|
||||||
4. Загрузку существующей серверной PDA на странице обновления.
|
|
||||||
5. Обновление `server_address` и `sync_servers` только по `root + device` без blockchain-ключа.
|
|
||||||
6. Корректное чтение нового формата `ServerProfileBlock` через общий PDA-модуль.
|
|
||||||
7. То, что актуальной точкой входа остаётся `shine-UI/server-ui.html`.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
1. Новые страницы открываются без JS-ошибок.
|
|
||||||
2. Создание серверной PDA проходит через общий модуль и пишет актуальный формат.
|
|
||||||
3. Обновление серверной PDA переиспользует существующую подпись LastBlockState и не требует blockchain-ключ.
|
|
||||||
4. Клиентский UI не ломается после перевода общего PDA-слоя на новый формат.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Кнопка настройки сервера и DEVNET topup
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
На экране `entry-settings-view` добавлена кнопка `Настроить свой сервер`, открывающая `server-ui.html` в новой вкладке.
|
|
||||||
На страницах серверного UI добавлена кнопка открытия `devnet-topup-view` в новой вкладке с автоматической передачей `wallet` из device-адреса.
|
|
||||||
|
|
||||||
- что именно проверять:
|
|
||||||
1. На странице настроек входа есть кнопка `Настроить свой сервер`.
|
|
||||||
2. Кнопка открывает `shine-UI/server-ui.html` в новой вкладке.
|
|
||||||
3. На страницах `create-server-pda.html` и `update-server-pda.html` есть кнопка `Открыть пополнение DEVNET`.
|
|
||||||
4. Если device public key заполнен, новая вкладка открывает `devnet-topup-view?wallet=...` с правильным адресом.
|
|
||||||
5. Если device-адрес не введён, серверный UI показывает понятную ошибку и не открывает пустую ссылку.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
1. Переход в серверный UI с клиентской страницы настроек работает.
|
|
||||||
2. Пополнение devnet из серверного UI открывается сразу на нужный адрес.
|
|
||||||
3. Основной клиентский UI и серверные страницы не получают JS-ошибок при загрузке.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Фикс DEVNET topup и автоподстановки пароля
|
|
||||||
|
|
||||||
- статус: pending
|
|
||||||
- кратко: исправлена ширина экрана `devnet-topup-view` после успешного пополнения и отключена нежелательная автоподстановка пароля в server UI и на экранах входа/регистрации.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
- Открыть страницу пополнения DEVNET, выполнить пополнение и убедиться, что после появления `Signature` экран не расширяется по ширине.
|
|
||||||
- Проверить, что кнопки на странице пополнения остаются аккуратными и не разъезжаются.
|
|
||||||
- Открыть `server-ui/update-server-pda.html`, загрузить PDA и убедиться, что поле пароля остаётся пустым.
|
|
||||||
- Проверить обычные экраны входа и регистрации: поле пароля не должно самопроизвольно заполняться длинной строкой.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
- Длинная transaction signature переносится по строкам внутри прежней ширины экрана.
|
|
||||||
- Кнопки сохраняют компактный mobile-first layout.
|
|
||||||
- Поля пароля пустые, пока пользователь сам ничего не вводил.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Диагностика ключей server PDA и баланс device
|
|
||||||
|
|
||||||
- статус: pending
|
|
||||||
- кратко: на странице обновления server PDA добавлена сверка ожидаемых ключей с уже загруженной PDA, предупреждение о неверном пароле, кнопка показа баланса device-аккаунта и уточнение, что create/update оплачиваются с deviceKey.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
- На `update-server-pda.html` загрузить существующую PDA и убедиться, что видны ожидаемые `root/blockchain/device` public key.
|
|
||||||
- Ввести правильный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи совпадают.
|
|
||||||
- Ввести неверный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи не совпали и пароль, вероятно, неверный.
|
|
||||||
- На `create-server-pda.html` и `update-server-pda.html` нажать `Показать / обновить баланс device` и убедиться, что баланс читается по текущему `devPub`.
|
|
||||||
- Повторить `update_user_pda` после увеличения `heap frame` и проверить, ушла ли ошибка `memory allocation failed`.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
- Пользователь видит, какие именно public key должны получиться для загруженной PDA.
|
|
||||||
- Ошибка неправильного пароля выявляется до отправки транзакции.
|
|
||||||
- Баланс device-кошелька читается прямо со страницы.
|
|
||||||
- Если проблема `OOM` была только в размере heap frame/compute budget клиента, `update_user_pda` начинает проходить.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Lazy-import Solana PDA: актуальный формат
|
|
||||||
|
|
||||||
- Краткое описание:
|
|
||||||
Серверный Java lazy-import пользователя из `shine_users` обновлён под актуальный формат `user_pda`. Убран RPC-фильтр по размеру PDA, добавлен разбор нового `ServerProfileBlock` (`block_type = 30`) без сохранения server-only полей в `solana_users`.
|
|
||||||
- Что проверять:
|
|
||||||
1. Взять логин пользователя, который существует в Solana PDA, но отсутствует в локальной таблице `solana_users`.
|
|
||||||
2. Выполнить вход этим логином через сервер.
|
|
||||||
3. Убедиться, что lazy-import подтянул пользователя из Solana.
|
|
||||||
4. Убедиться, что запись в `solana_users` создана с полями `login`, `blockchain_name`, `solana_key`, `blockchain_key`, `device_key`.
|
|
||||||
5. Убедиться, что отсутствие/наличие server-полей в PDA не ломает импорт.
|
|
||||||
- Ожидаемый результат:
|
|
||||||
1. Пользователь успешно находится и импортируется из Solana PDA независимо от фактического размера PDA.
|
|
||||||
2. Новый `ServerProfileBlock` не ломает парсер.
|
|
||||||
3. В БД не появляются лишние server-only поля.
|
|
||||||
- Статус: `pending`
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Pure Rust `shine_users` и `shine_login_guard`
|
|
||||||
|
|
||||||
Статус: `pending`
|
|
||||||
|
|
||||||
## Что сделано
|
|
||||||
|
|
||||||
- `shine_login_guard` переписан без Anchor на чистый Rust/Solana SDK.
|
|
||||||
- `shine_users` переписан без Anchor на чистый Rust/Solana SDK.
|
|
||||||
- Для `shine_users` введён новый instruction ABI без Anchor discriminator'ов.
|
|
||||||
- Для `shine_users` используются новые seed'ы:
|
|
||||||
- `user_login=` для `user_pda`
|
|
||||||
- `shine_users_economy_config` для economy PDA
|
|
||||||
- Формат блоков PDA синхронизирован:
|
|
||||||
- `SessionsBlock = 50`
|
|
||||||
- `TrustedStateBlock = 70`
|
|
||||||
- UI JS-модуль и Java lazy-import обновлены под новые seeds/ABI/коды блоков.
|
|
||||||
|
|
||||||
## Что проверить руками
|
|
||||||
|
|
||||||
1. В обычном UI выполнить регистрацию нового пользователя в Solana.
|
|
||||||
2. Проверить, что после регистрации читается новая `user_pda`.
|
|
||||||
3. В server UI выполнить создание server PDA.
|
|
||||||
4. В server UI выполнить update server PDA.
|
|
||||||
5. Проверить, что после update растёт `record_number`.
|
|
||||||
6. Проверить, что lazy-import на сервере читает новый формат PDA без ошибок.
|
|
||||||
7. Проверить, что старые Anchor discriminator'ы больше нигде не требуются.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
- Регистрация и update работают на новых чисто-rust программах.
|
|
||||||
- UI не использует старый Anchor ABI.
|
|
||||||
- Серверный Java parser читает новый формат PDA.
|
|
||||||
- Ошибок `out of memory` и anchor-specific падений больше нет.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# ESP32 Argon2/UI совместимость и экран результата
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
выравнивание derivation на `ESP32` с текущим `UI` по нормализации логина, совместимости `master secret`/`root.key`/`bch.key`/`dev.key`, а также правки экрана результата и progress bar.
|
|
||||||
|
|
||||||
- что именно проверять:
|
|
||||||
1. На `UI` и `ESP32` ввести один и тот же логин в разном регистре, например `Anya24`, и один и тот же непустой пароль.
|
|
||||||
2. Убедиться, что после нормализации логина на `ESP32` и `UI` получаются одинаковые:
|
|
||||||
`master secret`, `root`, `blockchain`, `device` в `Base58`.
|
|
||||||
3. Проверить режим пустого пароля:
|
|
||||||
`UI` и `ESP32` должны выдать одинаковые ключи в legacy-режиме.
|
|
||||||
4. Проверить, что пустой логин на `ESP32` не запускает расчёт и показывает сообщение об ошибке.
|
|
||||||
5. Проверить progress bar:
|
|
||||||
при непустом пароле полоса должна быть видна и двигаться.
|
|
||||||
6. Проверить экран результата:
|
|
||||||
сначала `Login`, затем `Password`, затем `Master secret` и ключи;
|
|
||||||
свайп вверх/вниз должен прокручивать длинный результат без артефактов.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
`ESP32` и `UI` считают одинаковый `master secret` и одинаковые ключи для одинаковых входных данных;
|
|
||||||
progress bar виден;
|
|
||||||
экран результата читаемый и корректно прокручивается.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
## Краткое описание
|
|
||||||
В `SHiNE-agent-bot-coder` для личных чатов добавлен режим одного редактируемого статусного сообщения. Бот принимает запрос, обновляет это сообщение по этапам обработки и в конце превращает его в финальный текстовый ответ. При длинном ответе допускается ещё одно дополнительное текстовое сообщение с продолжением. Голосовой ответ остаётся отдельным сообщением.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
1. Отправить в личный чат короткий текстовый запрос и убедиться, что бот не шлёт цепочку промежуточных сообщений, а редактирует одно сообщение до финального ответа.
|
|
||||||
2. Отправить в личный чат `voice` или `audio` и убедиться, что в том же сообщении последовательно видны этапы распознавания и выполнения.
|
|
||||||
3. Проверить длинный ответ, который не помещается в один Telegram message: должно получиться не больше двух текстовых сообщений.
|
|
||||||
4. Проверить, что `voice`-ответ приходит отдельным новым сообщением после текстового.
|
|
||||||
5. Проверить, что в `@shine_writing` по-прежнему логируются только итоговые `вопрос -> ответ`, без промежуточных статусов.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
- В личке основная переписка стала чище: промежуточные этапы живут в одном редактируемом сообщении.
|
|
||||||
- При длинном ответе бот не разбрасывает ответ на много сообщений.
|
|
||||||
- Канал `@shine_writing` работает по старой схеме без лишнего шума.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
`pending`
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
## Краткое описание
|
|
||||||
В локальный Telegram-бот `SHiNE-agent-bot-coder` добавлена команда `/settings`, которая сразу показывает текущие персональные настройки пользователя и список доступных команд для их изменения. В `/help` оставлена только ссылка на `/settings` без перечисления самих команд настроек. Также добавлен переключатель режима ответа в личке: один редактируемый статус или отдельные сообщения по этапам.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
1. Отправить `/help` и убедиться, что в справке есть `/settings`, но нет списка команд `/voice_*` и `/single_message_*`.
|
|
||||||
2. Отправить `/settings` и проверить, что бот показывает текущие значения:
|
|
||||||
- озвучивание финальных ответов;
|
|
||||||
- адаптацию текста перед озвучкой;
|
|
||||||
- режим одного редактируемого сообщения в личке.
|
|
||||||
3. По очереди переключить:
|
|
||||||
- `/voice_on` и `/voice_off`;
|
|
||||||
- `/voice_rewrite_on` и `/voice_rewrite_off`;
|
|
||||||
- `/single_message_on` и `/single_message_off`.
|
|
||||||
4. После каждого переключения снова вызвать `/settings` и убедиться, что статус изменился и сохранился.
|
|
||||||
5. При `/single_message_on` отправить обычный запрос в личку и проверить, что бот ведёт его через одно редактируемое сообщение.
|
|
||||||
6. При `/single_message_off` отправить обычный запрос в личку и проверить, что бот снова шлёт отдельные сообщения по этапам и отдельный финальный ответ.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
- `/settings` стал основной точкой входа для пользовательских настроек.
|
|
||||||
- `/help` стал короче и не дублирует список команд настроек.
|
|
||||||
- Режим ответа в личке реально переключается персонально для пользователя и сохраняется после перезапуска сервиса.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
`pending`
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
# Shine Payments: e2e после переписи без Anchor и добавления Q3
|
|
||||||
|
|
||||||
## Краткое описание
|
|
||||||
|
|
||||||
Нужно вручную и через вспомогательные CLI-проверки подтвердить, что программа `shine_payments` после:
|
|
||||||
|
|
||||||
- переписи на чистый `solana_program`;
|
|
||||||
- отказа от `programs/common`;
|
|
||||||
- добавления очереди `Q3`;
|
|
||||||
- обновления HTML UI;
|
|
||||||
|
|
||||||
корректно работает на devnet с новым `program id`.
|
|
||||||
|
|
||||||
Отличие от финального боевого сценария:
|
|
||||||
|
|
||||||
- вместо DAO-механики используется обычный кошелёк `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`, которому даны права DAO на изменение коэффициента и выдачу лимитов менеджеру.
|
|
||||||
|
|
||||||
## Что именно проверять
|
|
||||||
|
|
||||||
### 1. Подготовка окружения
|
|
||||||
|
|
||||||
Проверить и зафиксировать:
|
|
||||||
|
|
||||||
- новый keypair программы `shine_payments`;
|
|
||||||
- новый `program id`;
|
|
||||||
- обновление `program id` в HTML UI и связанных настройках;
|
|
||||||
- наличие deploy authority, которой можно закрыть старый buffer/programdata, если это технически доступно;
|
|
||||||
- адреса тестовых кошельков:
|
|
||||||
- DAO/базовый кошелёк;
|
|
||||||
- менеджер;
|
|
||||||
- покупатель 1;
|
|
||||||
- покупатель 2;
|
|
||||||
- получатели выплат.
|
|
||||||
|
|
||||||
### 2. Очистка/смена старой программы
|
|
||||||
|
|
||||||
Проверить один из сценариев:
|
|
||||||
|
|
||||||
- если возможно, закрыть старый `program buffer/programdata` текущими ключами;
|
|
||||||
- если закрытие невозможно или нецелесообразно, зафиксировать это и продолжить с новым `program id`.
|
|
||||||
|
|
||||||
Отдельно проверить, что старые PDA предыдущей версии не используются новой программой.
|
|
||||||
|
|
||||||
### 3. Деплой и init новой программы
|
|
||||||
|
|
||||||
Проверить:
|
|
||||||
|
|
||||||
- `cargo build-sbf` проходит;
|
|
||||||
- новая программа деплоится на devnet;
|
|
||||||
- `init` выполняется один раз на пустых PDA;
|
|
||||||
- после `init` читаются:
|
|
||||||
- `config`;
|
|
||||||
- `coef_limit`;
|
|
||||||
- `queues`;
|
|
||||||
- `inflow_vault`.
|
|
||||||
|
|
||||||
Сразу после `init` запросить состояние очередей и зафиксировать, что:
|
|
||||||
|
|
||||||
- `Q1`, `Q2`, `Q3` пустые;
|
|
||||||
- `tickets_total = 0`;
|
|
||||||
- `tickets_paid = 0`;
|
|
||||||
- все суммы равны `0`.
|
|
||||||
|
|
||||||
### 4. Проверка покупки билета
|
|
||||||
|
|
||||||
На минимальных суммах проверить:
|
|
||||||
|
|
||||||
1. покупку через `buy_ticket_usd`;
|
|
||||||
2. покупку через `buy_ticket_sol`;
|
|
||||||
3. при необходимости ещё один вызов базового `buy_ticket`.
|
|
||||||
|
|
||||||
После каждой покупки:
|
|
||||||
|
|
||||||
- запросить состояние `Q1`;
|
|
||||||
- убедиться, что создался следующий ticket;
|
|
||||||
- проверить рост:
|
|
||||||
- `q1_tickets_total`;
|
|
||||||
- `q1_sum_total_usd_cents`;
|
|
||||||
- убедиться, что деньги покупки ушли в `dao_wallet`, а не в `inflow_vault`.
|
|
||||||
|
|
||||||
### 5. Проверка DAO-управления
|
|
||||||
|
|
||||||
Проверить:
|
|
||||||
|
|
||||||
1. изменение коэффициента через `update_coef_limit`;
|
|
||||||
2. повторный запрос `coef_limit` и подтверждение нового значения;
|
|
||||||
3. выдачу менеджеру прав через `grant_manager_limits`:
|
|
||||||
- отдельно под `Q1`;
|
|
||||||
- отдельно под `Q2`;
|
|
||||||
- отдельно под `Q3`.
|
|
||||||
|
|
||||||
После выдачи лимитов:
|
|
||||||
|
|
||||||
- считать `manager_allowance_pda`;
|
|
||||||
- убедиться, что лимиты записаны отдельно по трём очередям.
|
|
||||||
|
|
||||||
### 6. Проверка manager_add_ticket
|
|
||||||
|
|
||||||
На минимальных суммах создать менеджерские тикеты:
|
|
||||||
|
|
||||||
1. один ticket в `Q1`;
|
|
||||||
2. один ticket в `Q2`;
|
|
||||||
3. один ticket в `Q3`.
|
|
||||||
|
|
||||||
После каждого добавления:
|
|
||||||
|
|
||||||
- запросить состояние очередей;
|
|
||||||
- проверить рост счётчиков и сумм именно у нужной очереди;
|
|
||||||
- проверить уменьшение соответствующего manager allowance.
|
|
||||||
|
|
||||||
### 7. Проверка приоритета очередей
|
|
||||||
|
|
||||||
Подтвердить очередность `step_payout`:
|
|
||||||
|
|
||||||
1. сначала выплачивается `Q1`;
|
|
||||||
2. затем `Q2`;
|
|
||||||
3. затем `Q3`.
|
|
||||||
|
|
||||||
Для этого:
|
|
||||||
|
|
||||||
- между шагами регулярно читать `queues`;
|
|
||||||
- фиксировать, какой именно ticket был следующим к выплате;
|
|
||||||
- убедиться, что при наличии pending в `Q1` программа не уходит в `Q2` или `Q3`.
|
|
||||||
|
|
||||||
### 8. Проверка частичных выплат
|
|
||||||
|
|
||||||
Перед выплатами пополнять `inflow_vault` только минимально достаточными суммами.
|
|
||||||
|
|
||||||
Нужно проверить:
|
|
||||||
|
|
||||||
1. частичную серию выплат, когда часть тикетов ещё остаётся pending;
|
|
||||||
2. дополнительную покупку билета в промежутке между выплатами;
|
|
||||||
3. повторную проверку приоритета после появления нового билета в `Q1`.
|
|
||||||
|
|
||||||
После каждого `step_payout`:
|
|
||||||
|
|
||||||
- запрашивать состояние очередей;
|
|
||||||
- проверять:
|
|
||||||
- рост `tickets_paid`;
|
|
||||||
- рост `sum_paid_usd_cents`;
|
|
||||||
- `is_paid = true` у погашенного ticket;
|
|
||||||
- правильный DAO multiplier:
|
|
||||||
- `Q1 -> 1x`;
|
|
||||||
- `Q2 -> 2x`;
|
|
||||||
- `Q3 -> 3x`.
|
|
||||||
|
|
||||||
### 9. Проверка финального добора
|
|
||||||
|
|
||||||
После частичных выплат:
|
|
||||||
|
|
||||||
- купить ещё один билет;
|
|
||||||
- допополнить `inflow_vault`;
|
|
||||||
- выполнить оставшиеся `step_payout` до полного погашения всех трёх очередей.
|
|
||||||
|
|
||||||
В конце:
|
|
||||||
|
|
||||||
- все pending ticket должны отсутствовать;
|
|
||||||
- все суммы paid должны совпасть с total по каждой очереди;
|
|
||||||
- если вызвать `step_payout` на пустых очередях, доступный остаток `inflow_vault` должен уйти в `dao_wallet`.
|
|
||||||
|
|
||||||
### 10. Финальный возврат лампортов
|
|
||||||
|
|
||||||
После завершения теста вернуть все доступные остатки, которые можно вернуть текущими полномочиями, на базовый кошелёк:
|
|
||||||
|
|
||||||
- `FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P`
|
|
||||||
|
|
||||||
Отдельно зафиксировать:
|
|
||||||
|
|
||||||
- что именно удалось вернуть;
|
|
||||||
- что именно нельзя вернуть без специальной инструкции закрытия или без deploy authority.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
- `buy_ticket_usd` и `buy_ticket_sol` создают ticket без ошибок чтения state;
|
|
||||||
- `Q3` работает наравне с `Q2`, но с третьим приоритетом;
|
|
||||||
- DAO может менять коэффициент и выдавать лимиты;
|
|
||||||
- менеджер может создавать билеты во все три очереди;
|
|
||||||
- `step_payout` соблюдает порядок `Q1 -> Q2 -> Q3`;
|
|
||||||
- DAO-множитель на выплатах равен `1x/2x/3x` для `Q1/Q2/Q3`;
|
|
||||||
- HTML UI и on-chain программа используют один и тот же актуальный `program id`;
|
|
||||||
- остатки средств после теста по максимуму возвращены на базовый DAO-кошелёк.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
- `done`
|
|
||||||
|
|
||||||
## Итог выполнения
|
|
||||||
|
|
||||||
- новый `shine_payments` задеплоен в devnet с `program id`:
|
|
||||||
- `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`
|
|
||||||
- старый `shine_payments`:
|
|
||||||
- `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR`
|
|
||||||
- закрыт, лампорты возвращены на базовый DAO-кошелёк
|
|
||||||
- HTML UI переведён на новый `program id`
|
|
||||||
- подтверждены:
|
|
||||||
- `init`
|
|
||||||
- `buy_ticket_usd`
|
|
||||||
- `buy_ticket_sol`
|
|
||||||
- `grant_manager_limits`
|
|
||||||
- `manager_add_ticket` для `Q1/Q2/Q3`
|
|
||||||
- `change_ticket_recipient`
|
|
||||||
- `update_coef_limit`
|
|
||||||
- `step_payout` по порядку `Q1 -> Q2 -> Q3`
|
|
||||||
- повторный возврат приоритета в `Q1` после новой покупки
|
|
||||||
- итоговые агрегаты очередей:
|
|
||||||
- `Q1 total=4, paid=4, sum_total=780, sum_paid=780`
|
|
||||||
- `Q2 total=1, paid=1, sum_total=60, sum_paid=60`
|
|
||||||
- `Q3 total=1, paid=1, sum_total=70, sum_paid=70`
|
|
||||||
- временные тестовые кошельки собраны обратно в базовый DAO-кошелёк
|
|
||||||
- в `inflow_vault` остался только rent-минимум PDA
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Клиентская Solana-регистрация после ухода от Anchor
|
|
||||||
|
|
||||||
## Краткое описание
|
|
||||||
|
|
||||||
Исправлен рассинхрон обычного клиентского UI с no-Anchor ABI программ:
|
|
||||||
|
|
||||||
- `shine_login_guard`
|
|
||||||
- `shine_users`
|
|
||||||
|
|
||||||
Исправлены клиентские вызовы:
|
|
||||||
|
|
||||||
1. Solana-предпроверка логина в обычном UI.
|
|
||||||
2. `init_users_economy_config` в обычном UI.
|
|
||||||
|
|
||||||
## Что проверять
|
|
||||||
|
|
||||||
1. На странице регистрации проверка свободного логина не выдаёт `InvalidInstructionData`.
|
|
||||||
2. Для свободного обычного логина отображается корректный статус без fallback-предупреждения про недоступную Solana-предпроверку.
|
|
||||||
3. Регистрация пользователя через обычный UI проходит до конца.
|
|
||||||
4. Страница `Solana: init регистрации` в обычном UI отправляет корректную транзакцию и не падает из-за старого Anchor discriminator.
|
|
||||||
|
|
||||||
## Ожидаемый результат
|
|
||||||
|
|
||||||
1. `shine_login_guard` принимает клиентский precheck.
|
|
||||||
2. `init_users_economy_config` из обычного UI совместим с текущей программой `shine_users`.
|
|
||||||
3. Обычный клиентский UI ведёт себя так же, как серверный UI, там где используется общий no-Anchor путь.
|
|
||||||
|
|
||||||
## Статус
|
|
||||||
|
|
||||||
- `pending`
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# ESP32 UI-прототип сабсервера SHiNE
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
для `Waveshare ESP32-S3-Touch-AMOLED-2.16` добавлен новый интерактивный UI-скетч сабсервера `SHiNE` с хранением данных в `NVS`, настройками `Wi-Fi`, настройками серверов, кошельком, экраном `QR/URI`, живой Solana-регистрацией и экраном входящих запросов. Логика PIN в коде сохранена, но вход по PIN во временной сборке отключён, чтобы не блокировать проверку остальных экранов. В текущей версии `Wi-Fi` подключается реально, адреса `API/RPC/WS` проверяются реально, баланс кошелька читается из `Solana RPC`, а регистрация отправляет `create_user_pda` в `shine_users`.
|
|
||||||
|
|
||||||
- что именно проверять:
|
|
||||||
1. Прошить режим `subserver-ui` и дождаться открытия главного экрана без PIN.
|
|
||||||
2. Проверить, что текст в заголовках, кнопках и статусах отображается читаемо; в текущей временной сборке допускается ASCII-транслитерация русского текста.
|
|
||||||
3. Открыть `Настройки` и убедиться, что показывается пометка о временно отключённом входе по PIN.
|
|
||||||
4. Открыть `Подключение -> Wi-Fi`, ввести `SSID` и пароль, нажать `Проверить`, дождаться реального подключения, затем перезагрузить устройство и проверить, что значения сохранились.
|
|
||||||
5. Открыть `Подключение -> Серверы`, проверить или изменить `API/RPC/WS`, нажать `Проверить` и убедиться, что показываются реальные статусы доступности, затем перезагрузить устройство и проверить сохранение значений.
|
|
||||||
6. Открыть `Аккаунт`, ввести логин, имя сабсервера и нажать `Сгенерировать`; проверить, что появились секрет и адрес кошелька, а после перезагрузки они не исчезают.
|
|
||||||
7. Открыть `Кошелёк`, нажать `Проверить` и убедиться, что баланс реально читается из `Solana RPC`; затем открыть `QR и URI` и проверить, что QR-код отрисовывается и сканируется как `solana:`-ссылка.
|
|
||||||
8. При необходимости отдельно проверить тестовые кнопки `+/- SOL`: они меняют локальный баланс для UX-сценариев, но после следующей реальной RPC-проверки баланс должен вернуться к сетевому значению.
|
|
||||||
9. Вернуться на главный экран и проверить, что до выполнения всех условий кнопка регистрации недоступна, а после выполнения становится доступной.
|
|
||||||
10. Выполнить регистрацию и убедиться, что статус меняется на `Сабсервер активен`, онлайн-статус становится активным, а на экране появляются краткие отпечатки `PDA/TX`.
|
|
||||||
11. После регистрации проверить через `Solana`/UI проекта, что `user_pda` для этого логина реально создана и соответствует `device`-адресу устройства.
|
|
||||||
12. Открыть `Запросы`, поочерёдно открыть оба демонстрационных запроса и проверить, что кнопки `Разрешить` и `Отклонить` меняют их статус.
|
|
||||||
13. При необходимости открыть `Настройки -> Сменить PIN` и убедиться, что новый PIN сохраняется, хотя вход по PIN временно не используется на старте.
|
|
||||||
14. Выполнить `Полный сброс` и убедиться, что все поля, секрет, баланс, онлайн и регистрация очищаются.
|
|
||||||
|
|
||||||
- ожидаемый результат:
|
|
||||||
новый `ESP32`-скетч стабильно запускается, показывает читаемый интерфейс хотя бы в ASCII-транслитерации, сохраняет данные во внутренней памяти устройства, реально подключается к `Wi-Fi`, реально проверяет `API/RPC/WS`, реально читает баланс из `Solana RPC`, рисует рабочий `QR` для `solana:`-URI и позволяет вручную пройти полный сценарий on-chain регистрации сабсервера.
|
|
||||||
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# ESP32 авто-прошивка shine_subserver_ui
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
добавлен исполняемый скрипт `flash_shine_subserver_ui.sh`, который автоматически ищет USB-порт `ESP32` и запускает заливку прошивки `shine_subserver_ui` без ручного указания `PORT`.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Подключить плату `ESP32` по USB.
|
|
||||||
2. Перейти в папку `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/test-device/`.
|
|
||||||
3. Запустить `./flash_shine_subserver_ui.sh`.
|
|
||||||
4. Убедиться, что скрипт сам показывает найденный порт и успешно запускает compile/upload.
|
|
||||||
- ожидаемый результат:
|
|
||||||
скрипт без ручного ввода порта находит `ESP32`, печатает найденный `/dev/ttyACM*` или `/dev/ttyUSB*` и заливает `shine_subserver_ui`.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# ESP32 тест рендера текста
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
добавлен отдельный диагностический скетч `text_render_test`, который показывает один экран с несколькими вариантами вывода текста: встроенный шрифт `Arduino_GFX`, `U8g2` ASCII, `U8g2` кириллица и кнопки с подписями. Скрипт нужен для изоляции проблемы, когда на экране видны только цветные кнопки и блоки, но не видно ни одной буквы.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Прошить режим `text-test`.
|
|
||||||
2. Проверить, виден ли заголовок `TEXT TEST 123`.
|
|
||||||
3. Проверить, видны ли строки `A`, `B`, `C`, `D`.
|
|
||||||
4. Проверить, видны ли подписи на трёх нижних кнопках: `BTN 1`, `abc123`, `Русский`.
|
|
||||||
5. Сравнить, какой из способов вывода реально отображается, а какой нет.
|
|
||||||
- ожидаемый результат:
|
|
||||||
хотя бы один вариант вывода текста становится видим на экране, что позволяет локализовать проблему до конкретного шрифта или способа рендера.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# ESP32 PIN-клавиатура: подписи кнопок
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
в UI-скетче `shine_subserver_ui` изменена отрисовка подписей кнопок. Вместо малого шрифта теперь используется более стабильный шрифт с явным центрированием текста внутри кнопок, чтобы на экране ввода PIN и других экранах не пропадали цифры и надписи.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Включить устройство и дождаться экрана ввода PIN.
|
|
||||||
2. Убедиться, что на всех серых кнопках видны цифры `0-9`, `Отмена` и `OK`.
|
|
||||||
3. Открыть другие экраны с кнопками (`Главный экран`, `Wi-Fi`, `Серверы`, `Настройки`) и убедиться, что подписи отображаются и не уезжают за границы кнопок.
|
|
||||||
- ожидаемый результат:
|
|
||||||
подписи кнопок стабильно видны сразу после старта, текст визуально центрирован, пустых серых кнопок без цифр и названий нет.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# ESP32 папка тестовых скетчей
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
добавлена отдельная папка `test_sketches/` с изолированными диагностическими скетчами для экрана `ESP32-S3-Touch-AMOLED-2.16`: тест рендера текста через `Arduino_GFX`, тест геометрии кнопок и минимальный тест `LVGL`.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Запустить `./burn.sh gfx-text-test` и убедиться, что прошивается тест текста из новой папки.
|
|
||||||
2. Запустить `./burn.sh gfx-layout-test` и проверить нижние ряды кнопок.
|
|
||||||
3. Запустить `./burn.sh lvgl-basic-test` и проверить, что `LVGL` показывает текст и кнопки.
|
|
||||||
4. Убедиться, что новая папка не мешает сборке `subserver-ui`.
|
|
||||||
- ожидаемый результат:
|
|
||||||
тестовые скетчи лежат отдельно от основного UI, шьются отдельными режимами и позволяют быстро проверять разные гипотезы по экрану без правок в `shine_subserver_ui`.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# ESP32 LVGL interaction test
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
добавлен отдельный скетч `lvgl_interaction_test` на `LVGL`: экран с 9 кнопками, touch-вводом и нижней статусной строкой. При нажатии на кнопку на экране и в `Serial` показывается, какая именно кнопка нажата и сколько нажатий уже было.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Прошить режим `lvgl-interaction-test`.
|
|
||||||
2. Убедиться, что виден заголовок, подзаголовок, 9 кнопок и нижняя статусная панель.
|
|
||||||
3. Поочерёдно нажать разные кнопки.
|
|
||||||
4. Проверить, что нижняя строка меняется на `Pressed: <button> (#N)`.
|
|
||||||
5. Проверить, что touch устойчиво работает по всей сетке кнопок.
|
|
||||||
- ожидаемый результат:
|
|
||||||
`LVGL` стабильно рисует плотный экран с множеством кнопок, а нажатия корректно обрабатываются и визуально подтверждаются без глюков позиционирования.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# ESP32 LVGL touch debug test
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
добавлен отдельный диагностический скетч `lvgl_touch_debug_test`, который одновременно показывает сырые координаты touch, маркер точки касания и одну большую кнопку `LVGL`. Он нужен, чтобы отделить проблему raw-touch от проблемы доставки событий в `LVGL`.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Прошить режим `lvgl-touch-debug-test`.
|
|
||||||
2. Коснуться экрана в разных местах.
|
|
||||||
3. Проверить, меняется ли текст `RAW pressed` и координаты `x/y`.
|
|
||||||
4. Проверить, появляется ли розовый маркер точки касания.
|
|
||||||
5. Проверить, срабатывает ли большая кнопка `Tap Here` и меняется ли строка `LVGL button clicked`.
|
|
||||||
- ожидаемый результат:
|
|
||||||
становится ясно, работает ли сам touch-драйвер, правильно ли приходят координаты и доходит ли нажатие до кнопки `LVGL`.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# ESP32 LVGL official based test
|
|
||||||
|
|
||||||
- краткое описание фичи:
|
|
||||||
добавлен отдельный скетч `lvgl_official_based_test`, который строится на максимально близкой к официальному `05_LVGL_Widgets` инициализации `display + touch + LVGL`, но вместо официального demo рисует наш компактный экран с кнопками и статусом нажатия.
|
|
||||||
- что именно проверять:
|
|
||||||
1. Прошить режим `lvgl-official-based-test`.
|
|
||||||
2. Убедиться, что экран отображается без артефактов по краям.
|
|
||||||
3. Нажать разные кнопки и проверить, меняется ли нижняя строка `Pressed: ...`.
|
|
||||||
4. Проверить, идут ли координаты touch в `Serial`.
|
|
||||||
- ожидаемый результат:
|
|
||||||
если официальный каркас инициализации действительно является рабочей базой, то на этом тесте должны заработать и touch, и кнопки, и исчезнуть визуальные артефакты, которые были в наших самодельных `LVGL`-тестах.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# LVGL Russian font test
|
|
||||||
|
|
||||||
- Краткое описание: тест кастомного `LVGL`-шрифта с кириллицей на базе рабочего `LVGL + subserver touch` контура.
|
|
||||||
- Что проверять:
|
|
||||||
- на экране видны русские заголовки и подписи без транслита;
|
|
||||||
- отображаются буквы `Ё/ё`;
|
|
||||||
- видны кнопки `Статус`, `Подключение`, `Кошелёк`, `Запросы`, `Настройки`, `Регистрация`, `Разрешить`, `Отклонить`, `Назад`;
|
|
||||||
- длинная кнопка `Проверка переноса русского текста` отображается читаемо;
|
|
||||||
- строка `Нажато:` меняется при клике;
|
|
||||||
- строка `Касание:` меняется при касании.
|
|
||||||
- Ожидаемый результат: кириллица стабильно отображается на `LVGL`-экране и не ломает touch.
|
|
||||||
- Статус: pending
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# ESP32 nav minimal test
|
|
||||||
|
|
||||||
- Краткое описание: минимальный UI-прототип для сабсервера на базе `LVGL + subserver touch`, с Wi-Fi flow, серверными адресами и общим экраном редактирования текста.
|
|
||||||
- Что проверять:
|
|
||||||
- стартует экран `HOME`;
|
|
||||||
- на `HOME` видны реальное значение сабсервера или `subserver not set`, реальное значение логина или `login not set`, при отсутствии секрета строка `secret not set`, а также `STATUS`, верхний правый блок с процентом батареи, иконкой батареи и индикатором Wi-Fi, кнопка `SETTINGS` и нижняя подпись `SHiNE subserver (v.0.18)`;
|
|
||||||
- строка Wi-Fi на `HOME` корректно показывает одно из состояний:
|
|
||||||
- `Wi-Fi (not configured) not configured`
|
|
||||||
- `Wi-Fi (<saved_ssid>) disconnected`
|
|
||||||
- `Wi-Fi (<current_ssid>) connected`
|
|
||||||
- пока открыт `HOME`, статус сам обновляется без перехода на другие экраны;
|
|
||||||
- кнопка `SETTINGS` открывает `SETTINGS_MENU`;
|
|
||||||
- свайп влево на `HOME` открывает `SETTINGS_MENU`;
|
|
||||||
- в `SETTINGS_MENU` сначала видны только `Wi-Fi` и `Server`;
|
|
||||||
- обе видимые карточки меню одного цвета;
|
|
||||||
- свайп вверх показывает `Server` и `Account`;
|
|
||||||
- свайп вниз возвращает `Wi-Fi` и `Server`;
|
|
||||||
- свайп вправо из `SETTINGS_MENU` возвращает на `HOME`;
|
|
||||||
- нажатие `Wi-Fi` открывает `WIFI_SCREEN`;
|
|
||||||
- `SELECT NETWORK` запускает скан;
|
|
||||||
- после скана показывается список доступных SSID;
|
|
||||||
- выбор SSID открывает общий экран редактирования текста для пароля;
|
|
||||||
- на этом экране видно старое значение, курсор стоит в конце;
|
|
||||||
- две верхние служебные строки над полем ввода отсутствуют;
|
|
||||||
- при вводе пароля Wi-Fi текст показывается открыто, без точек;
|
|
||||||
- большая клавиатура реально видна на экране и занимает большую часть высоты;
|
|
||||||
- буквы разбиты на 2 страницы;
|
|
||||||
- режим символов тоже разбит на 2 страницы;
|
|
||||||
- на правой странице кнопки стоят в ровных вертикальных колонках;
|
|
||||||
- свайп влево/вправо на экране ввода переключает страницы клавиатуры;
|
|
||||||
- при этом свайп страниц клавиатуры срабатывает только из нижней клавиатурной зоны, а не из верхней части экрана;
|
|
||||||
- при переключении `ABC/123` и `SHIFT` уже введённый текст не пропадает;
|
|
||||||
- при свайпе между левой и правой половиной клавиатуры уже введённый текст тоже не пропадает, в том числе для цифр, символов и заглавных букв;
|
|
||||||
- визуальный курсор в поле ввода не показывается;
|
|
||||||
- новые символы всегда дописываются только в конец строки;
|
|
||||||
- основные 3 ряда клавиш и нижний служебный ряд стали выше;
|
|
||||||
- внизу остаётся отдельная тёмная полоса с версией `SHiNE subserver (v.0.18)`, а рамка клавиатурного блока заканчивается выше неё;
|
|
||||||
- одно непрерывное касание вызывает не более одного действия кнопки;
|
|
||||||
- скольжение пальцем по клавиатуре не нажимает подряд несколько клавиш;
|
|
||||||
- медленный свайп по экрану не должен превращаться в случайное нажатие кнопки;
|
|
||||||
- `ABC/123`, `SHIFT`, `DEL`, `SAVE`, `CANCEL` работают;
|
|
||||||
- при успехе SSID и пароль сохраняются, а `HOME` показывает `Wi-Fi connected`;
|
|
||||||
- при ошибке показывается `Connection failed`;
|
|
||||||
- `CLEAR SAVED WI-FI` очищает сохранённые настройки;
|
|
||||||
- если сеть была ранее успешно сохранена, после потери связи устройство автоматически пытается переподключиться;
|
|
||||||
- первые повторные попытки идут раз в `10` секунд, а после долгого отсутствия связи интервал увеличивается до `30` секунд;
|
|
||||||
- нажатие `Server` открывает `SERVER_SCREEN`;
|
|
||||||
- в `SERVER_SCREEN` видны и редактируются два значения:
|
|
||||||
- `https://api.devnet.solana.com`
|
|
||||||
- `https://shineup.me`
|
|
||||||
- нажатие `SOLANA RPC` открывает общий экран редактирования;
|
|
||||||
- нажатие `SHINE SERVER` открывает общий экран редактирования;
|
|
||||||
- после `SAVE` новые адреса сохраняются в NVS;
|
|
||||||
- нажатие `Account` открывает `ACCOUNT_SCREEN`;
|
|
||||||
- `ACCOUNT_SCREEN` показывает 3 кнопки:
|
|
||||||
- `Login (<value|not set>)`
|
|
||||||
- `Subserver (<value|not set>)`
|
|
||||||
- `Secret (<*****|not set>)`
|
|
||||||
- `Login` открывает общий экран редактирования и сохраняется в NVS;
|
|
||||||
- `Subserver` открывает промежуточный экран с `USE SUBSERVER1` и `EDIT MANUALLY`;
|
|
||||||
- `USE SUBSERVER1` возвращает стандартное значение `subserver1`;
|
|
||||||
- `EDIT MANUALLY` открывает общий экран редактирования и сохраняет значение в NVS;
|
|
||||||
- `Secret` открывает экран-заглушку, где сказано, что настройка ещё не реализована;
|
|
||||||
- `Secret` теперь открывает меню секрета с показом секрета, ручным вводом и генерацией;
|
|
||||||
- при смене `login` сохранённый секрет сбрасывается в `not set`;
|
|
||||||
- во время генерации секрета есть `CANCEL` и подтверждение остановки;
|
|
||||||
- при отмене генерации старый секрет, если он был, не должен теряться;
|
|
||||||
- свайп вправо из внутренних экранов возвращает в `SETTINGS_MENU`;
|
|
||||||
- свайп вправо из `ACCOUNT_SUBSERVER_SCREEN` и `ACCOUNT_SECRET_SCREEN` возвращает в `ACCOUNT_SCREEN`;
|
|
||||||
- если во время реального свайпа палец проходит по кнопке, это не должно открывать кнопку как обычный `click`.
|
|
||||||
- Ожидаемый результат: новый скетч даёт чистый навигационный каркас и уже умеет настраивать Wi-Fi и серверные адреса на самой ESP32.
|
|
||||||
- Статус: pending
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Deeplink ссылки профиля и связей
|
|
||||||
|
|
||||||
- краткое описание:
|
|
||||||
Исправлена загрузка UI по прямым ссылкам вида `https://shineup.me/shine.<login>` и `https://shineup.me/shine.<login>/links` через добавление корневого `<base href="/">` в основной `index.html`.
|
|
||||||
- что проверять:
|
|
||||||
1. Открыть прямую ссылку на профиль в новой вкладке: `https://shineup.me/shine.<login>`.
|
|
||||||
2. Открыть прямую ссылку на связи в новой вкладке: `https://shineup.me/shine.<login>/links`.
|
|
||||||
3. Повторить оба сценария в состоянии гостя.
|
|
||||||
4. Повторить оба сценария в состоянии, когда в браузере залогинен другой пользователь.
|
|
||||||
- ожидаемый результат:
|
|
||||||
1. Страница загружается напрямую без поломки ассетов и без ухода на неверный экран.
|
|
||||||
2. Открывается профиль/связи именно пользователя из URL.
|
|
||||||
3. Для гостя экран открывается в read-only режиме.
|
|
||||||
4. Для залогиненного другого пользователя URL не подменяется на текущую сессию.
|
|
||||||
- статус:
|
|
||||||
pending
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Быстрое скрытие экрана звонка и остановка гудков при отклонении
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
- Исправлена нижняя подпись вкладки личных сообщений на `личные`.
|
||||||
|
- Исправлена логика звонка, когда одна из входящих сессий отклоняет вызов до принятия: исходящая сессия теперь должна прекращать гудки даже если ранее `RINGING` пришёл от другой входящей сессии.
|
||||||
|
- На устройстве, где пользователь нажимает отмену/сброс звонка, экран вызова теперь скрывается сразу локально, без ожидания сетевого ответа.
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
- В нижней панели с 5 кнопками подпись первой кнопки должна отображаться как `личные`.
|
||||||
|
- Сценарий: один пользователь звонит, у второго входящий вызов приходит на несколько устройств; на любом одном входящем устройстве нажать `Сбросить`.
|
||||||
|
- Проверить, что у звонящего сразу прекращаются гудки и экран вызова корректно завершается.
|
||||||
|
- Проверить, что на устройстве, где нажали `Сбросить` или `Положить трубку`, overlay звонка исчезает сразу, без заметной задержки.
|
||||||
|
- Проверить, что после принятия звонка на одном устройстве поздние отмены с других устройств не ломают уже выбранную пару соединения.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- Подпись в нижней панели корректная.
|
||||||
|
- При отклонении входящего звонка любым устройством звонящего не оставляет в состоянии бесконечных гудков.
|
||||||
|
- Локальный экран звонка скрывается мгновенно после нажатия кнопки отмены.
|
||||||
|
- Уже зафиксированный сценарий соединения после `ACCEPT` не сбивается другими сессиями.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- `pending`
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Отключение устаревшего TURN-узла `45.136.124.227`
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- из конфигурации звонков убран устаревший TURN-узел `45.136.124.227:3478`;
|
||||||
|
- основным и единственным выдаваемым TURN-узлом оставлен `93.170.12.154:3478`.
|
||||||
|
- что проверять:
|
||||||
|
- сделать несколько тестовых звонков между разными устройствами/сетями;
|
||||||
|
- убедиться, что звонок доходит до стадии соединения и появляется звук;
|
||||||
|
- убедиться, что в логах `CallDeliveryReport` больше не фигурирует `45.136.124.227`.
|
||||||
|
- ожидаемый результат:
|
||||||
|
- клиентам больше не выдаётся устаревший TURN-адрес;
|
||||||
|
- звонки не заваливаются из-за попыток использовать отключённый TURN-узел.
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Фикс самообрыва звонка из-за `stop_call` push своей же сессии
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- исправлена ситуация, когда активный звонок мог оборваться сразу после соединения;
|
||||||
|
- причина была в том, что `stop_call` push, предназначенный для других сессий того же пользователя, обрабатывался и в исходной сессии.
|
||||||
|
- что проверять:
|
||||||
|
- открыть несколько вкладок/устройств одного пользователя;
|
||||||
|
- принять звонок на одной сессии;
|
||||||
|
- убедиться, что активная сессия не обрывает звонок сразу после соединения;
|
||||||
|
- убедиться, что лишние сессии при этом закрывают свой локальный экран звонка.
|
||||||
|
- ожидаемый результат:
|
||||||
|
- звонок не завершается сразу после `call_connected`;
|
||||||
|
- `accepted_on_other_device` и связанные `stop_call` события больше не убивают исходную активную сессию.
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Фикс привязки call push к целевой sessionId
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- push-события `incoming_call` и `stop_call` теперь помечаются целевой `sessionId`;
|
||||||
|
- UI и service worker обрабатывают call push только для своей целевой сессии;
|
||||||
|
- `stop_call` для лишних сессий закрывает локальный экран тихо, без обратных сигналов и без лишних тех-сообщений.
|
||||||
|
- что проверять:
|
||||||
|
- держать несколько сессий одного пользователя в одном браузере/на одном origin;
|
||||||
|
- позвонить этому пользователю и убедиться, что входящий экран закрывается корректно только на целевых сессиях;
|
||||||
|
- после `ACCEPT` одной сессии остальные должны тихо убрать экран вызова и не ломать выбранную пару;
|
||||||
|
- после отмены входящей сессией исходящая сессия должна централизованно завершить сценарий.
|
||||||
|
- ожидаемый результат:
|
||||||
|
- push одного session endpoint больше не влияет на чужие сессии этого же origin;
|
||||||
|
- исчезают ложные `stop_call_push:accepted_on_other_device` и `terminal_call_signal_150` на неправильных сессиях.
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# ESP32 homeserver: заявки на подключение устройств
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
- на ESP32 homeserver в `SETTINGS` добавлен первый пункт `Device requests`, который появляется только после авторизации homeserver в SHiNE;
|
||||||
|
- экран показывает список активных pairing-заявок, позволяет открыть каждую заявку и подтвердить или отклонить её;
|
||||||
|
- формат кода подключения изменён на `10` цифр и показывается как `5` пар.
|
||||||
|
|
||||||
|
- что проверять:
|
||||||
|
- на обычном клиенте и в wallet-plugin код отображается как `XX XX XX XX XX`;
|
||||||
|
- на доверенном веб-клиенте экран `Подключить по коду` показывает все активные заявки без поля ручного ввода;
|
||||||
|
- на ESP32 после успешной homeserver-авторизации в `SETTINGS` появляется пункт `Device requests` первым;
|
||||||
|
- `REFRESH` реально загружает активные заявки;
|
||||||
|
- на экране видно две плитки, список листается вертикально;
|
||||||
|
- client-session заявка после `YES` подключается с передачей только `device key`;
|
||||||
|
- wallet-session заявка после `YES` подключается без передачи ключей, через выпуск отдельной wallet-session;
|
||||||
|
- `NO` отклоняет заявку и она исчезает из списка активных.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- все три клиента используют единый формат кода;
|
||||||
|
- активные заявки видны без ручного ввода кода;
|
||||||
|
- ESP32 может одобрять и отклонять живые pairing-заявки пользователя.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Тестовые deploy-контуры `test2.shineup.me` и `test.shineup.me`
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- default deploy-задачи `deployServer` и `deployUI` переведены на основной тестовый сервер `test2.shineup.me`;
|
||||||
|
- production-задачи вынесены в `deployServerProduction` и `deployUIProduction`;
|
||||||
|
- `test.shineup.me` оставлен как резервный тестовый сервер без обычного deploy по умолчанию.
|
||||||
|
|
||||||
|
- что проверять:
|
||||||
|
- `./gradlew deployServer` и `./gradlew deployUI` действительно направлены на `test2.shineup.me`;
|
||||||
|
- `./gradlew deployServerProduction` и `./gradlew deployUIProduction` больше не используются как default;
|
||||||
|
- `https://test2.shineup.me` открывает UI;
|
||||||
|
- `wss://test2.shineup.me/ws` отвечает;
|
||||||
|
- на `test2.shineup.me` после deploy есть копия продовой `shine.sqlite` и `.bch`.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- default deploy идёт только на `test2.shineup.me`;
|
||||||
|
- production `shineup.me` меняется только после отдельного подтверждения;
|
||||||
|
- `test.shineup.me` остаётся резервным тестовым сервером;
|
||||||
|
- тестовый deploy не гоняет удалённые тесты и не создаёт пустую БД.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Регистрация: FAQ и режим пароля из 12 слов
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
- на экране регистрации добавлен блок частых вопросов с переходом на отдельный экран справки;
|
||||||
|
- добавлен альтернативный режим ввода пароля через 12 полей-слов в кошелёчном формате, которые склеиваются в одну строку без изменения API;
|
||||||
|
- такой же режим добавлен и на экран входа по логину и паролю.
|
||||||
|
|
||||||
|
- что проверять:
|
||||||
|
- на стартовом экране открыть `Зарегистрироваться`;
|
||||||
|
- убедиться, что внизу экрана есть кнопки FAQ;
|
||||||
|
- открыть несколько вопросов и проверить возврат обратно на регистрацию;
|
||||||
|
- включить галочку `Представить пароль в виде 12 слов`;
|
||||||
|
- убедиться, что появляется сетка с нумерованными полями в 3 колонки;
|
||||||
|
- ввести часть слов, перейти дальше и проверить, что шаг подтверждения и генерация ключей работают;
|
||||||
|
- выключить галочку и проверить, что пароль остаётся собранным в одном поле;
|
||||||
|
- открыть экран входа по паролю и повторить те же проверки для режима `12 слов`;
|
||||||
|
- пройти регистрацию до шага оплаты без ошибок интерфейса.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- FAQ открывается отдельным экраном и содержит понятные ответы;
|
||||||
|
- режим `12 слов` не ломает регистрацию и вход и даёт тот же поток, что и обычный пароль;
|
||||||
|
- пароль не отправляется в новом формате, а продолжает использоваться как одна строка.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Временная бесплатная загрузка аватара в Arweave
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
Добавлены два временных `Test...` API для бесплатной загрузки маленьких аватаров в Arweave через серверный кошелёк с лимитом `3` загрузки на пользователя. В UI мастера смены аватара добавлен пункт `Залить аватар бесплатно`.
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
1. Пользователь с активной сессией открывает редактирование профиля.
|
||||||
|
2. По нажатию на аватар открывается мастер `Сменить аватар`.
|
||||||
|
3. В мастере есть пункт `Залить аватар бесплатно`.
|
||||||
|
4. До первой загрузки UI показывает остаток `3 из 3`.
|
||||||
|
5. Маленький JPEG/PNG/WebP после уменьшения до файла <= `128 KB` успешно уходит через `TestUploadFreeAvatar`.
|
||||||
|
6. После загрузки приходит `txId`, и аватар сохраняется в профиль как `avatar.ar`.
|
||||||
|
7. Остаток уменьшается: `2`, `1`, `0`.
|
||||||
|
8. На четвёртой попытке сервер отвечает понятной ошибкой про исчерпанный бесплатный лимит.
|
||||||
|
9. Если итоговый уменьшенный файл всё ещё > `128 KB`, UI не отправляет его и показывает понятную ошибку.
|
||||||
|
10. Если серверный Arweave JWK/path не настроен, UI получает понятную ошибку временной функции.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- первые 3 маленькие аватарки загружаются через серверный Arweave-кошелёк;
|
||||||
|
- после каждой успешной загрузки `ava` в профиле указывает на новый `txId`;
|
||||||
|
- после исчерпания лимита дальнейшая бесплатная загрузка блокируется без записи в профиль;
|
||||||
|
- обычная загрузка через свой Arweave-кошелёк продолжает работать отдельно.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
pending
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Исправление chatId личных сообщений через lowercase
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
- В клиентском UI SHiNE для личных сообщений технический `chatId` теперь канонизируется через `trim().toLowerCase()` при приёме DM, открытии чата и восстановлении сообщений из IndexedDB.
|
||||||
|
- Цель: исключить рассинхрон, когда unread-индикатор есть, а входящие сообщения конкретного собеседника не видны из-за разного регистра логина.
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
- Отправить личные сообщения между двумя пользователями, у одного из которых логин отображается с заглавными буквами.
|
||||||
|
- Убедиться, что входящие сообщения показываются внутри открытого чата, а не только в общем unread-индикаторе.
|
||||||
|
- Перезагрузить страницу и проверить, что история чата после гидрации из IndexedDB остаётся в одном диалоге.
|
||||||
|
- Проверить, что переход в чат из списка диалогов и из графа связей открывает тот же диалог без дублирования.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- Все сообщения одного собеседника попадают в один и тот же DM-чат независимо от регистра логина.
|
||||||
|
- Общий unread, список диалогов и содержимое открытого чата совпадают между собой.
|
||||||
|
- После перезагрузки UI не появляется отдельный дубль диалога с тем же логином в другом регистре.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# AGENTS
|
||||||
|
|
||||||
|
## Документация DM в этой папке
|
||||||
|
|
||||||
|
- Основной актуальный документ по личным сообщениям:
|
||||||
|
- `README.md`
|
||||||
|
- Его считать единственным источником истины по текущей реализованной логике DM.
|
||||||
|
|
||||||
|
## Черновик будущих вложений
|
||||||
|
|
||||||
|
- Файл `Черновик_будущих_DM_вложений.md` не является актуальной спецификацией.
|
||||||
|
- В нём описан только ранний черновик того, как когда-то планировались:
|
||||||
|
- формат вложений в DM;
|
||||||
|
- внешние и внутренние поля вложения;
|
||||||
|
- предполагаемая механика загрузки файлов.
|
||||||
|
- Эта схема не была реализована в таком виде и может существенно измениться в будущем.
|
||||||
|
- Любые решения по текущему коду, протоколу и UI нельзя принимать по этому черновику.
|
||||||
|
- Если есть расхождение между `README.md` и черновиком вложений, верным всегда считается `README.md`.
|
||||||
@@ -1,269 +1,203 @@
|
|||||||
# Личные сообщения (DM): как это устроено
|
# Личные сообщения (DM)
|
||||||
|
|
||||||
## Коротко (для быстрого понимания)
|
## Текущее состояние
|
||||||
|
|
||||||
Личные сообщения в SHiNE сейчас работают как пара **подписанных клиентом блоков** в формате `SHiNE_dm2`:
|
Сейчас в проекте реализованы:
|
||||||
|
|
||||||
- тип `1` — входящее сообщение для собеседника;
|
- новый формат контентных личных сообщений `SHiNE_DM`;
|
||||||
- тип `2` — исходящая копия того же сообщения для автора.
|
- ревизии сообщений через `revisionTimeMs`;
|
||||||
|
- редактирование сообщения через повторную отправку той же логической пары;
|
||||||
|
- удаление сообщения через пустую ревизию;
|
||||||
|
- `upsert` последней версии сообщения на сервере.
|
||||||
|
|
||||||
Оба блока отправляются вместе одной операцией (`SendMessagePair` / `ReceiveOutcomingMessage`) и либо сохраняются оба, либо не сохраняются вовсе.
|
Сейчас в проекте **не реализованы**:
|
||||||
Дальше сервер доставляет их по активным сессиям целевого логина событием `SignedMessageArrived`, а клиент подтверждает доставку на конкретную сессию через `AckSessionDelivery`.
|
|
||||||
|
|
||||||
Подтверждение прочтения также идёт парой блоков:
|
- вложения в DM;
|
||||||
|
- upload/download файлов для DM;
|
||||||
|
- UI-кнопка прикрепления файла;
|
||||||
|
- серверное хранение файловых связей для DM.
|
||||||
|
|
||||||
- тип `3` — «прочитано» для исходящего сообщения автора;
|
Черновик будущих вложений вынесен отдельно:
|
||||||
- тип `4` — зеркальная копия для второй стороны.
|
|
||||||
|
|
||||||
UI чата строится на этих типах: текстовые сообщения (1/2), read-receipt (3/4), непрочитанные, галочки и история.
|
- `Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
|
||||||
|
|
||||||
---
|
## Общая схема
|
||||||
|
|
||||||
## Подробно
|
Личное сообщение по-прежнему отправляется парой signed-блоков:
|
||||||
|
|
||||||
## 1) Общая схема потока
|
- `type=1` — входящий блок для получателя;
|
||||||
|
- `type=2` — исходящая копия для отправителя.
|
||||||
|
|
||||||
1. Клиент формирует текст сообщения и строит **2 подписанных блока** (`type=1` и `type=2`) с одинаковыми `fromLogin/toLogin/timeMs/nonce`.
|
Read-receipt пока остаются в legacy-формате:
|
||||||
2. Клиент отправляет оба блока в одном RPC: `SendMessagePair` (алиас: `ReceiveOutcomingMessage`).
|
|
||||||
3. Сервер:
|
|
||||||
- парсит оба блока;
|
|
||||||
- валидирует пару;
|
|
||||||
- проверяет существование `from/to` пользователей и подписи;
|
|
||||||
- атомарно сохраняет пару в `signed_messages_v2`.
|
|
||||||
4. Сервер доставляет блоки в активные сессии целевого логина событием `SignedMessageArrived`.
|
|
||||||
5. Клиент, получив событие, кладёт сообщение в локальный чат и отправляет `AckSessionDelivery(messageKey)`.
|
|
||||||
6. При открытии чата клиент отправляет read-receipt (пара `type=3/4`) для непрочитанных входящих.
|
|
||||||
|
|
||||||
## 2) Формат signed DM-блока (`SHiNE_dm2`)
|
- `type=3` — входящее подтверждение прочтения;
|
||||||
|
- `type=4` — исходящая копия подтверждения.
|
||||||
|
|
||||||
Префикс: `SHiNE_dm2` (ASCII).
|
Ключи сообщения:
|
||||||
|
|
||||||
Далее поля (big-endian):
|
- `baseKey = fromLogin|toLogin|timeMs|nonce`
|
||||||
|
|
||||||
1. `toLoginLen` (`u8`) + `toLogin` (ASCII, 1..60);
|
|
||||||
2. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, 1..60);
|
|
||||||
3. `timeMs` (`u64`);
|
|
||||||
4. `nonce` (`u32`);
|
|
||||||
5. `messageType` (`u16`);
|
|
||||||
6. `payloadLen` (`u16`);
|
|
||||||
7. `payloadBytes` (`1..4096`);
|
|
||||||
8. `signature` (`64 bytes`, Ed25519).
|
|
||||||
|
|
||||||
Ограничения:
|
|
||||||
|
|
||||||
- полный пакет: до `8192` байт;
|
|
||||||
- `messageType` сейчас допустим только `1..4`.
|
|
||||||
|
|
||||||
## 3) Типы DM-сообщений
|
|
||||||
|
|
||||||
- `1` (`TYPE_INCOMING_TEXT`) — входящий текст для получателя.
|
|
||||||
- `2` (`TYPE_OUTGOING_COPY`) — исходящая копия в истории автора.
|
|
||||||
- `3` (`TYPE_READ_INCOMING`) — read-receipt (входящий тип для пары квитанции).
|
|
||||||
- `4` (`TYPE_READ_OUTGOING_COPY`) — зеркальная копия read-receipt.
|
|
||||||
|
|
||||||
Правило пары:
|
|
||||||
|
|
||||||
- первый блок должен быть нечётным (`1` или `3`);
|
|
||||||
- второй должен быть ровно `+1` (`2` или `4`);
|
|
||||||
- ключевые поля пары совпадают: `toLogin/fromLogin/timeMs/nonce`.
|
|
||||||
|
|
||||||
## 4) Ключи сообщений
|
|
||||||
|
|
||||||
- `baseKey = from|to|timeMs|nonce`
|
|
||||||
- `messageKey = baseKey|messageType`
|
- `messageKey = baseKey|messageType`
|
||||||
|
|
||||||
Эти ключи используются:
|
Логический идентификатор письма задаётся парой:
|
||||||
|
|
||||||
- для дедупликации;
|
- `timeMs`
|
||||||
- для связи read-receipt с исходным сообщением;
|
- `nonce`
|
||||||
- для ACK доставки по сессии.
|
|
||||||
|
|
||||||
## 5) RPC и события
|
Эти поля не меняются при редактировании или удалении. Меняется только:
|
||||||
|
|
||||||
## `SendMessagePair` (алиас `ReceiveOutcomingMessage`)
|
- `revisionTimeMs`
|
||||||
|
- содержимое `encryptedBody`
|
||||||
|
|
||||||
Запрос:
|
Сервер хранит только последнюю версию записи для каждого `messageKey`.
|
||||||
|
|
||||||
```json
|
## Формат контентного DM: `SHiNE_DM`
|
||||||
{
|
|
||||||
"op": "SendMessagePair",
|
|
||||||
"requestId": "req-1",
|
|
||||||
"payload": {
|
|
||||||
"incomingBlobB64": "<base64 signed block type 1 or 3>",
|
|
||||||
"outgoingBlobB64": "<base64 signed block type 2 or 4>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Успешный ответ:
|
Префикс бинарного блока:
|
||||||
|
|
||||||
```json
|
- `SHiNE_DM`
|
||||||
{
|
|
||||||
"op": "SendMessagePair",
|
|
||||||
"requestId": "req-1",
|
|
||||||
"status": 200,
|
|
||||||
"ok": true,
|
|
||||||
"payload": {
|
|
||||||
"baseKey": "from|to|time|nonce",
|
|
||||||
"incomingKey": "from|to|time|nonce|1",
|
|
||||||
"outgoingKey": "from|to|time|nonce|2",
|
|
||||||
"deliveredWsSessions": 2,
|
|
||||||
"deliveredWebPushSessions": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## `SignedMessageArrived` (server event)
|
Поля идут в big-endian порядке:
|
||||||
|
|
||||||
Событие в сессию получателя содержит:
|
1. `formatVersionMajor` (`u8`) = `1`
|
||||||
|
2. `formatVersionMinor` (`u8`) = `0`
|
||||||
|
3. `toLoginLen` (`u8`) + `toLogin` (ASCII, `1..60`)
|
||||||
|
4. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, `1..60`)
|
||||||
|
5. `timeMs` (`u64`)
|
||||||
|
6. `nonce` (`u32`)
|
||||||
|
7. `messageType` (`u16`) — только `1` или `2`
|
||||||
|
8. `revisionTimeMs` (`u64`)
|
||||||
|
9. `attachmentsCount` (`u8`)
|
||||||
|
10. `encryptedBodyLen` (`u32`)
|
||||||
|
11. `encryptedBody` (`bytes`)
|
||||||
|
12. `signature` (`64 bytes`, Ed25519)
|
||||||
|
|
||||||
- `messageKey`, `baseKey`;
|
### Ограничения
|
||||||
- `fromLogin`, `toLogin`, `targetLogin`;
|
|
||||||
- `messageType`, `timeMs`, `nonce`;
|
|
||||||
- `blobB64`;
|
|
||||||
- `backlog` (признак догрузки из очереди).
|
|
||||||
|
|
||||||
## `AckSessionDelivery`
|
- `attachmentsCount` сейчас всегда должен быть `0`
|
||||||
|
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
|
||||||
|
- `revisionTimeMs` не может быть отрицательным
|
||||||
|
|
||||||
Запрос:
|
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
|
||||||
|
|
||||||
```json
|
- `ATTACHMENTS_DISABLED`
|
||||||
{
|
|
||||||
"op": "AckSessionDelivery",
|
|
||||||
"requestId": "ack-1",
|
|
||||||
"payload": {
|
|
||||||
"messageKey": "from|to|time|nonce|1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ: `status=200`, echo `messageKey`.
|
## Legacy read-receipt: `SHiNE_dm2`
|
||||||
|
|
||||||
## 6) Хранение на сервере (SQLite)
|
Подтверждения прочтения `type=3/4` пока используют старый контейнер `SHiNE_dm2`:
|
||||||
|
|
||||||
Основные таблицы:
|
1. `toLoginLen` (`u8`) + `toLogin`
|
||||||
|
2. `fromLoginLen` (`u8`) + `fromLogin`
|
||||||
|
3. `timeMs` (`u64`)
|
||||||
|
4. `nonce` (`u32`)
|
||||||
|
5. `messageType` (`u16`) — `3` или `4`
|
||||||
|
6. `payloadLen` (`u16`)
|
||||||
|
7. `payloadBytes`
|
||||||
|
8. `signature`
|
||||||
|
|
||||||
1. `signed_messages_v2` — сами DM-блоки типов `1/2/3/4`:
|
## Редактирование
|
||||||
- `message_key` (PK),
|
|
||||||
- `base_key`,
|
|
||||||
- `target_login`,
|
|
||||||
- `from_login`, `to_login`,
|
|
||||||
- `time_ms`, `nonce`, `message_type`,
|
|
||||||
- `raw_block`,
|
|
||||||
- `source_api`, `origin_session_id`,
|
|
||||||
- `receipt_ref_base_key`, `receipt_ref_type`.
|
|
||||||
2. `signed_message_session_delivery` — доставка по сессиям:
|
|
||||||
- составной PK `(message_key, session_id)`,
|
|
||||||
- `delivered` (0/1),
|
|
||||||
- `delivered_at_ms`, `created_at_ms`.
|
|
||||||
|
|
||||||
Примечание: историческая таблица `signed_direct_messages_history` в БД присутствует как legacy-слой, но текущий рабочий поток DM v2 опирается на `signed_messages_v2` + `signed_message_session_delivery`.
|
Редактирование делается новой отправкой той же логической пары сообщения:
|
||||||
|
|
||||||
## 7) Доставка и backlog
|
- `timeMs` и `nonce` остаются теми же;
|
||||||
|
- `messageType` остаётся `1/2`;
|
||||||
|
- `revisionTimeMs` становится больше;
|
||||||
|
- `encryptedBody` содержит новую версию текста.
|
||||||
|
|
||||||
- При сохранении пары сервер пытается сразу доставить в онлайн-сессии.
|
Если на сервер приходит более старая ревизия, она игнорируется.
|
||||||
- Для офлайн/недоступных сессий остаётся pending-запись доставки в таблице `signed_message_session_delivery`.
|
|
||||||
- При подключении сессии сервер автоматически вызывает `dispatchPendingForSession`:
|
|
||||||
- для новой сессии регистрирует все существующие сообщения адресата как «недоставленные»;
|
|
||||||
- отправляет **все** pending через WebSocket событием `SignedMessageArrived(backlog=true)`;
|
|
||||||
- лимита на количество сообщений нет — передаётся вся история без ограничений.
|
|
||||||
- Клиент дедублирует входящие через `knownMessageKeys`: если `messageKey` уже есть локально — игнорирует.
|
|
||||||
- После получения клиент отправляет `AckSessionDelivery`, чтобы отметить `delivered=1` в таблице доставки.
|
|
||||||
|
|
||||||
## 8) Read-receipt логика
|
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
|
||||||
|
|
||||||
Когда клиент открывает чат:
|
## Удаление
|
||||||
|
|
||||||
1. ищет входящие `messageType=1` без `readReceiptSent`;
|
Удаление личного сообщения делается как новая ревизия того же сообщения:
|
||||||
2. для каждого отправляет read-receipt как пару `type=3/4`;
|
|
||||||
3. после успешной отправки помечает `readReceiptSent`.
|
|
||||||
|
|
||||||
Сервер для read-receipt хранит ссылку на исходное сообщение:
|
- `timeMs` и `nonce` остаются прежними;
|
||||||
|
- `revisionTimeMs` увеличивается;
|
||||||
|
- `attachmentsCount = 0`;
|
||||||
|
- `encryptedBodyLen = 0`;
|
||||||
|
- `encryptedBody` пустой.
|
||||||
|
|
||||||
- `receipt_ref_base_key`;
|
В UI такое сообщение не показывается.
|
||||||
- `receipt_ref_type`.
|
|
||||||
|
|
||||||
Есть уникальность, чтобы не плодить дубликаты receipt на один и тот же `baseKey` для одного `target_login`.
|
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
|
||||||
|
|
||||||
## 9) Логика UI-клиента
|
## Поведение сервера
|
||||||
|
|
||||||
### Хранилище сообщений
|
Для контентных DM сервер:
|
||||||
|
|
||||||
- In-memory: `state.chats[chatId]` — массив сообщений по каждому диалогу.
|
1. принимает пару signed-блоков `type=1/2`;
|
||||||
- Персистентно: IndexedDB база `shine-ui-messages-v1`, object store `messages`, ключ `messageKey`.
|
2. валидирует формат, подпись и совпадение ключевых полей пары;
|
||||||
- `chatId` для `type=1` — `fromLogin`, для `type=2` — `toLogin`.
|
3. проверяет, что для обеих сторон пары совпадают:
|
||||||
|
- `fromLogin`
|
||||||
|
- `toLogin`
|
||||||
|
- `timeMs`
|
||||||
|
- `nonce`
|
||||||
|
- `revisionTimeMs`
|
||||||
|
- `encryptedBody`
|
||||||
|
4. делает `upsert` последней версии в `signed_messages_v2`;
|
||||||
|
5. сбрасывает pending-доставку по сессиям для новой ревизии;
|
||||||
|
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
|
||||||
|
|
||||||
### Жизненный цикл при старте/подключении
|
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
|
||||||
|
|
||||||
1. `hydrateMessagesFromStore()` — читает все сообщения из IndexedDB в `state.chats` (до WebSocket-соединения).
|
## Хранение в БД
|
||||||
2. После установки WebSocket-сессии сервер присылает backlog (`SignedMessageArrived(backlog=true)`) для всех недоставленных сообщений.
|
|
||||||
3. Клиент дедублирует через `knownMessageKeys` — уже имеющиеся в IndexedDB игнорируются.
|
|
||||||
4. Новые сообщения в реальном времени приходят теми же WebSocket-событиями.
|
|
||||||
|
|
||||||
### Очистка при выходе и смене пользователя
|
Основная таблица:
|
||||||
|
|
||||||
- При любом логауте (`terminateCurrentSession`) IndexedDB с сообщениями **удаляется полностью**.
|
- `signed_messages_v2`
|
||||||
- При входе нового пользователя через QR — IndexedDB удаляется явно до вызова `terminateCurrentSession`.
|
|
||||||
- При входе нового пользователя через логин/пароль — IndexedDB удаляется в `registration-keys-view.js` прямо перед `authorizeSession()`.
|
|
||||||
- Это гарантирует: при любом способе входа старые сообщения предыдущего пользователя не попадут к следующему.
|
|
||||||
|
|
||||||
### UI-поведение
|
Для контентных DM в ней используются:
|
||||||
|
|
||||||
- непрочитанные считаются по `from='in' && unread=true`;
|
- `message_key`
|
||||||
- доставка/прочтение исходящих:
|
- `base_key`
|
||||||
- `firstTick` — сообщение принято сервером,
|
- `target_login`
|
||||||
- `secondTick` — пришло подтверждение прочтения;
|
- `from_login`
|
||||||
- при открытии диалога UI автопрокручивает ленту в самый низ;
|
- `to_login`
|
||||||
- после отправки нового сообщения UI сразу прокручивает ленту вниз.
|
- `time_ms`
|
||||||
|
- `nonce`
|
||||||
|
- `message_type`
|
||||||
|
- `revision_time_ms`
|
||||||
|
- `raw_block`
|
||||||
|
- `created_at_ms`
|
||||||
|
|
||||||
## 10) Синхронизация личных сообщений между серверами
|
Отдельных таблиц файлов для DM сейчас нет.
|
||||||
|
|
||||||
Когда пользователи зарегистрированы на разных серверах SHiNE, серверы должны синхронизировать DM между собой.
|
## События и доставка
|
||||||
|
|
||||||
### Общий принцип
|
Запрос на отправку по WebSocket остаётся прежним:
|
||||||
|
|
||||||
- Сервер A получает DM-блок, адресованный пользователю на сервере B.
|
- `SendMessagePair`
|
||||||
- Сервер A пересылает этот блок серверу B (межсерверный relay).
|
- `ReceiveOutcomingMessage` как алиас
|
||||||
- Сервер B сохраняет блок и доставляет его в активные сессии получателя.
|
|
||||||
- Серверы, между которыми идёт синхронизация, задаются списком `sync_servers` в PDA пользователя-сервера.
|
|
||||||
|
|
||||||
### Что синхронизируется
|
Клиент отправляет:
|
||||||
|
|
||||||
- Все DM-блоки типов `1/2` (текстовые сообщения) и `3/4` (read-receipt).
|
- `incomingBlobB64`
|
||||||
- Синхронизация двусторонняя: оба сервера должны уметь принимать и пересылать блоки.
|
- `outgoingBlobB64`
|
||||||
|
|
||||||
### Идемпотентность
|
Событие в активные сессии:
|
||||||
|
|
||||||
- Блоки имеют уникальный `message_key` (`from|to|timeMs|nonce|type`).
|
- `SignedMessageArrived`
|
||||||
- Повторная доставка одного и того же блока безопасна — дедупликация происходит по `message_key`.
|
|
||||||
|
|
||||||
### Статус реализации
|
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
|
||||||
|
|
||||||
Межсерверная синхронизация DM **пока не реализована**. Текущая версия работает только в рамках одного сервера. Это задача для следующего этапа.
|
Подтверждение доставки в сессию:
|
||||||
|
|
||||||
---
|
- `AckSessionDelivery`
|
||||||
|
|
||||||
## 11) Инварианты (обязательно соблюдать при доработках)
|
## Правила UI
|
||||||
|
|
||||||
1. Пара блоков (1/2 или 3/4) должна оставаться атомарной.
|
UI сейчас работает так:
|
||||||
2. `messageKey`/`baseKey` формат должен быть совместим с текущей логикой дедупликации и receipt.
|
|
||||||
3. Доставка должна оставаться **по сессиям** с явным `AckSessionDelivery`.
|
|
||||||
4. Read-receipt не должен отправляться многократно на один и тот же `baseKey`.
|
|
||||||
5. Любые изменения DM-логики в коде должны сразу отражаться в этом документе.
|
|
||||||
|
|
||||||
## 12) Ключевые файлы реализации
|
- показывает только текст `encryptedBody`;
|
||||||
|
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
|
||||||
|
- не показывает удалённые сообщения;
|
||||||
|
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
||||||
|
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
||||||
|
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
||||||
|
- не показывает и не принимает вложения.
|
||||||
|
|
||||||
- UI:
|
## Что обязательно помнить
|
||||||
- `shine-UI/js/services/auth-service.js`
|
|
||||||
- `shine-UI/js/app.js`
|
- вложения в DM сейчас отключены на уровне протокола и UI;
|
||||||
- `shine-UI/js/state.js`
|
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
|
||||||
- `shine-UI/js/pages/chat-view.js`
|
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|
||||||
- Сервер:
|
|
||||||
- `shine-server-net-protocol/.../messages/SignedMessageBlock.java`
|
|
||||||
- `shine-server-net-protocol/.../messages/SignedMessagesCore.java`
|
|
||||||
- `shine-server-net-protocol/.../messages/Net_SendMessagePair_Handler.java`
|
|
||||||
- `shine-server-net-protocol/.../messages/SignedMessagesRealtime.java`
|
|
||||||
- `shine-server-net-protocol/.../messages/Net_AckSessionDelivery_Handler.java`
|
|
||||||
- БД:
|
|
||||||
- `shine-server-db/src/main/java/shine/db/DatabaseInitializer.java`
|
|
||||||
- `shine-server-db/src/main/java/shine/db/dao/SignedMessagesV2DAO.java`
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Черновик будущих вложений в DM
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
|
||||||
|
Этот документ описывает только ранний черновик идеи.
|
||||||
|
|
||||||
|
Сейчас в проекте **нет** поддержки вложений в личных сообщениях:
|
||||||
|
|
||||||
|
- в реализованном формате `SHiNE_DM` поле `attachmentsCount` пока всегда должно быть `0`;
|
||||||
|
- UI не показывает кнопку прикрепления файлов;
|
||||||
|
- сервер не принимает upload файлов для DM;
|
||||||
|
- сервер не раздаёт специальные DM-файлы по отдельным endpoints;
|
||||||
|
- сервер не хранит отдельные файловые связи для личных сообщений.
|
||||||
|
|
||||||
|
Этот документ нужен только для того, чтобы рядом с актуальной документацией было явно видно:
|
||||||
|
|
||||||
|
- какие идеи обсуждались;
|
||||||
|
- что это **не реализовано**;
|
||||||
|
- что формат, хранение и способ загрузки потом могут сильно измениться.
|
||||||
|
|
||||||
|
## Что обсуждалось
|
||||||
|
|
||||||
|
Рассматривался такой общий подход:
|
||||||
|
|
||||||
|
- у контентного DM есть внешний список вложений;
|
||||||
|
- во внешнем формате лежат только технические данные;
|
||||||
|
- человекочитаемые данные о файле живут внутри зашифрованного тела сообщения;
|
||||||
|
- один и тот же blob-файл теоретически мог бы переиспользоваться в нескольких сообщениях.
|
||||||
|
|
||||||
|
Черновой вариант внешнего списка:
|
||||||
|
|
||||||
|
- `attachmentsCount`
|
||||||
|
- далее для каждого вложения:
|
||||||
|
- `encFileHashSHA256` (`32 bytes`)
|
||||||
|
- `encFileSize` (`u64`)
|
||||||
|
|
||||||
|
Черновой вариант внутреннего маркера в тексте:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<<file:file-format(1.0):type|fileName|origSize|origHashB64u|encHashB64u|encSize|keyB64u|nonceB64u>>
|
||||||
|
```
|
||||||
|
|
||||||
|
Где обсуждались поля:
|
||||||
|
|
||||||
|
- `type`
|
||||||
|
- `fileName`
|
||||||
|
- `origSize`
|
||||||
|
- `origHashB64u`
|
||||||
|
- `encHashB64u`
|
||||||
|
- `encSize`
|
||||||
|
- `keyB64u`
|
||||||
|
- `nonceB64u`
|
||||||
|
|
||||||
|
## Что может измениться
|
||||||
|
|
||||||
|
В будущем могут измениться любые части идеи:
|
||||||
|
|
||||||
|
- сам бинарный формат;
|
||||||
|
- способ привязки файлов к сообщению;
|
||||||
|
- момент загрузки файла относительно отправки сообщения;
|
||||||
|
- серверное хранение blob-файлов;
|
||||||
|
- права доступа к скачиванию;
|
||||||
|
- способ рендера вложения в UI.
|
||||||
|
|
||||||
|
Именно поэтому этот файл не надо воспринимать как актуальную спецификацию.
|
||||||
|
|
||||||
|
## Источник истины на сейчас
|
||||||
|
|
||||||
|
Актуальное состояние личных сообщений описано только в:
|
||||||
|
|
||||||
|
- `Dev_Docs/Personal_Messages/README.md`
|
||||||
|
|
||||||
|
Если между этим черновиком и основным README есть расхождение, верным считается `README.md`.
|
||||||
@@ -90,7 +90,7 @@ UserPdaRecordV1
|
|||||||
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
| `3` | `BlockchainRegistryBlock` | Один или несколько блокчейнов пользователя. |
|
||||||
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
| `30` | `ServerProfileBlock` | Серверные данные пользователя. |
|
||||||
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
| `40` | `AccessServersBlock` | Серверы доступа/relay. |
|
||||||
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и саб-серверы. |
|
| `50` | `SessionsBlock` | Опубликованные пользовательские сессии и homeserver-ы. |
|
||||||
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
| `70` | `TrustedStateBlock` | Счетчик trusted-связей. |
|
||||||
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
| `255` | `ReservedBlock` | Зарезервировано, пока не используется. |
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ SessionRecord
|
|||||||
| Значение | Смысл |
|
| Значение | Смысл |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| `1` | Обычная пользовательская сессия. |
|
| `1` | Обычная пользовательская сессия. |
|
||||||
| `100` | Саб-сервер пользователя. |
|
| `100` | Homeserver пользователя. |
|
||||||
|
|
||||||
Правила:
|
Правила:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Аудит безопасности Solana-программ SHiNE — выпуск 2 (11.06.2026)
|
||||||
|
|
||||||
|
Повторный независимый аудит после исправления всех 4 находок первого отчёта
|
||||||
|
(`Solana-audit-by-Claude-File5-9июня2026.md`). Код перечитан целиком:
|
||||||
|
|
||||||
|
- `shine_login_guard` (183 строки) — stateless-классификатор логинов;
|
||||||
|
- `shine_users` (1069 строк) — реестр пользователей, PDA-записи, подписи, экономика лимитов;
|
||||||
|
- `shine_payments` (1381 строка) — очереди тикетов, выплаты из вольта, оракул Pyth.
|
||||||
|
|
||||||
|
Перебраны классы атак: подмена аккаунтов/PDA, авторизация и подписи, арифметика и
|
||||||
|
переполнения, валидация оракула, экономика, реентранси, griefing/DoS, **алиасинг
|
||||||
|
аккаунтов (передача одного аккаунта в несколько слотов инструкции)**.
|
||||||
|
|
||||||
|
## Статус прошлых находок (все закрыты)
|
||||||
|
|
||||||
|
- 🔴 Critical #1 (economy-config PDA в `shine_users`) — закрыто: `validate_users_economy_config_pda` проверяет и адрес, и `owner == program_id`, и вызывается перед чтением и в create, и в update.
|
||||||
|
- 🔴 Critical #2 (singleton-PDA в `shine_payments`) — закрыто: `validate_singleton_state_pda` проверяет точный адрес + `owner == id()` во всех инструкциях (`update_coef_limit`, `grant_manager_limits`, `buy_ticket*`, `manager_add_ticket`, `step_payout`, `change_ticket_recipient`).
|
||||||
|
- 🟠 Medium (валидация Pyth) — закрыто: пин адреса аккаунта `PYTH_SOL_USD_ACCOUNT`, проверка `owner == pyth_receiver`, разбор официальным `PriceUpdateV2`, `get_price_no_older_than` с проверкой `feed_id`, проверка возраста и доверительного интервала (`ORACLE_MAX_CONFIDENCE_PPM`).
|
||||||
|
- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` в обеих программах переведён на «создание поверх предзаполненного» (allocate + assign + добор ренты).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 HIGH (НОВОЕ) — `shine_payments`: тикет с `recipient_wallet == inflow_vault` навсегда замораживает все выплаты — ✅ ИСПРАВЛЕНО (11.06.2026)
|
||||||
|
|
||||||
|
Закрыто: равенство `recipient == inflow_vault` запрещено во всех точках задания
|
||||||
|
получателя — `buy_ticket_by_purchase_usd` (через `config.inflow_vault`),
|
||||||
|
`process_manager_add_ticket` и `process_change_ticket_recipient` (через
|
||||||
|
`find_single_pda(INFLOW_VAULT_SEED)`). Дополнительно в `transfer_from_vault` добавлена
|
||||||
|
защита по умолчанию `require!(vault.key != recipient.key)`. Документация —
|
||||||
|
`doc/programs/shine_payments.md` §10.1. Историческое описание находки ниже.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Где
|
||||||
|
`transfer_from_vault` (строки 1258–1268) переводит лампорты из вольта прямой
|
||||||
|
манипуляцией балансами (вольт — PDA без приватного ключа, обычный system-перевод
|
||||||
|
невозможен):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> ProgramResult {
|
||||||
|
if amount == 0 { return Ok(()); }
|
||||||
|
let mut vault_lamports = vault.try_borrow_mut_lamports()?; // займ #1
|
||||||
|
let mut recipient_lamports = recipient.try_borrow_mut_lamports()?; // займ #2
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
В `step_payout` (строка 849) получатель — это `ticket.recipient_wallet`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
transfer_from_vault(inflow_vault_pda, ticket_recipient_wallet, ticket_lamports)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
А `recipient_wallet` нигде не валидируется при создании тикета:
|
||||||
|
`buy_ticket*` (строки 696/711/725 → 1031), `manager_add_ticket` (строка 765),
|
||||||
|
`change_ticket_recipient` (строка 900) — берут его «как есть» из аргументов.
|
||||||
|
|
||||||
|
### Суть атаки (алиасинг аккаунта)
|
||||||
|
В Solana, если один и тот же аккаунт передан в инструкцию в нескольких слотах,
|
||||||
|
рантайм отдаёт для всех слотов **один и тот же** `RefCell` (механизм дублей).
|
||||||
|
Поэтому если `ticket.recipient_wallet` равен адресу `inflow_vault` PDA, то в
|
||||||
|
`step_payout` аккаунт вольта попадает и в слот `inflow_vault_pda`, и в слот
|
||||||
|
`ticket_recipient_wallet`. Тогда внутри `transfer_from_vault`:
|
||||||
|
|
||||||
|
- `vault.try_borrow_mut_lamports()` — берёт mutable-займ (успех);
|
||||||
|
- `recipient.try_borrow_mut_lamports()` — это **тот же** аккаунт → второй
|
||||||
|
mutable-займ → `Err(AccountBorrowFailed)` → `?` возвращает ошибку → инструкция
|
||||||
|
падает.
|
||||||
|
|
||||||
|
### Почему это «заморозка всего», а не один тикет
|
||||||
|
Выплаты идут строго по возрастанию индекса. `step_payout` всегда обслуживает
|
||||||
|
сначала очередь Q1 (если в ней есть pending), затем Q2, затем Q3, и в каждой —
|
||||||
|
ровно «следующий неоплаченный» тикет (`paid + 1`). Тикет с `recipient == vault`:
|
||||||
|
|
||||||
|
- не может быть оплачен (`step_payout` всегда падает на нём);
|
||||||
|
- не может быть пропущен (нет механизма «skip»);
|
||||||
|
- блокирует все тикеты после него в своей очереди;
|
||||||
|
- если он в Q1 — блокирует обслуживание Q2 и Q3 (до них очередь не доходит);
|
||||||
|
- лампорты вольта (накопленные регистрационные комиссии) перестают выплачиваться
|
||||||
|
и не уходят в DAO (слив в DAO происходит только когда `pending == 0` по всем
|
||||||
|
очередям, а это состояние недостижимо).
|
||||||
|
|
||||||
|
### Эксплуатация (тривиальная, перестановочная)
|
||||||
|
Q1 — публичная очередь (`buy_ticket` доступен любому). Атакующий покупает **один**
|
||||||
|
дешёвый тикет Q1, указав `recipient_wallet = <адрес inflow_vault PDA>`. Адрес вольта
|
||||||
|
детерминирован и публичен (`find_single_pda(INFLOW_VAULT_SEED)`). С этого момента вся
|
||||||
|
подсистема выплат и средства вольта заморожены за стоимость одного тикета + ренты.
|
||||||
|
|
||||||
|
Дополнительно: даже при защите на этапе покупки остаётся вектор через
|
||||||
|
`change_ticket_recipient` (строка 900) — владелец любого своего неоплаченного тикета
|
||||||
|
может выставить `new_recipient_wallet = vault` позже.
|
||||||
|
|
||||||
|
### Класс и серьёзность
|
||||||
|
Класс: «account aliasing / duplicate-account mutable borrow» + отсутствие
|
||||||
|
валидации адреса получателя. Прямой кражи средств нет, но это перманентный
|
||||||
|
отказ в обслуживании (availability) с блокировкой средств вольта, триггер —
|
||||||
|
копеечный и доступен анонимно. Оценка: **HIGH**.
|
||||||
|
|
||||||
|
### Рекомендуемый фикс
|
||||||
|
Запретить `recipient`, равный адресу вольта, во всех точках, где он задаётся, чтобы
|
||||||
|
тикет с таким получателем вообще не мог появиться:
|
||||||
|
|
||||||
|
1. в `buy_ticket_by_purchase_usd` — `require!(recipient_wallet != config.inflow_vault, …)`
|
||||||
|
(config уже прочитан);
|
||||||
|
2. в `process_manager_add_ticket` — сверять с `find_single_pda(INFLOW_VAULT_SEED).0`;
|
||||||
|
3. в `process_change_ticket_recipient` — то же для `new_recipient_wallet`.
|
||||||
|
|
||||||
|
Дополнительно (defense-in-depth) — в `transfer_from_vault` явно
|
||||||
|
`require!(vault.key != recipient.key, …)` с понятной ошибкой, чтобы любой будущий
|
||||||
|
вызов был защищён от алиасинга. Этого `require` недостаточно как единственной меры
|
||||||
|
(тикет всё равно застрял бы), поэтому основная защита — на входе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 LOW / INFO — наблюдения без прямой эксплуатации
|
||||||
|
|
||||||
|
### L1. `change_ticket_recipient` и `buy_ticket` не проверяют получателя на «опасные» адреса
|
||||||
|
Связано с HIGH выше; после фикса основной проблемы стоит заодно зафиксировать
|
||||||
|
правило «получатель не должен совпадать с системными PDA программы».
|
||||||
|
|
||||||
|
### L2. Гонка за логином (first-come) в `shine_users`
|
||||||
|
Адрес `user_pda` выводится из логина. После закрытия griefing-подсева остаётся
|
||||||
|
обычное состязание: увидев в мемпуле регистрацию `alice`, атакующий может
|
||||||
|
зарегистрировать `alice` со своим `root_key` первым. On-chain это решается только
|
||||||
|
commit-reveal; для текущей модели — приемлемый риск, отметить как известный.
|
||||||
|
|
||||||
|
### L3. `step_payout` без slippage-параметра
|
||||||
|
Выплата считается по текущей цене оракула без верхней границы лампортов. Цена
|
||||||
|
ограничена возрастом (120с) и доверительным интервалом (10%), аккаунт оракула
|
||||||
|
запинен — манипуляция маловероятна, но при резком движении цены SOL объём выплаты
|
||||||
|
в лампортах плавает. Риск низкий; при желании добавить верхнюю границу на шаг.
|
||||||
|
|
||||||
|
### L4. Экономическая устойчивость вольта (дизайн, не баг)
|
||||||
|
Деньги за покупку тикетов (`buy_ticket`) уходят на `dao_wallet`, а выплаты в
|
||||||
|
`step_payout` идут из `inflow_vault`, который наполняется **регистрационными
|
||||||
|
комиссиями** `shine_users`. Если поток регистраций меньше обязательств по выплатам,
|
||||||
|
вольт истощается и выплаты останавливаются (без потери средств, но с остановкой
|
||||||
|
сервиса). Это свойство экономической модели — стоит явно держать в уме и
|
||||||
|
мониторить баланс вольта/обязательств.
|
||||||
|
|
||||||
|
### L5. Заполнение Q1 до лимита как мягкий DoS
|
||||||
|
`buy_ticket` блокируется при `q1_sum_total >= limit_usd_cents`. Атакующий может
|
||||||
|
наполнить Q1 своими тикетами и приостановить покупки. Дорого (тратит SOL в DAO и
|
||||||
|
ренту) и его же тикеты потом оплачиваются из вольта, поэтому это скорее
|
||||||
|
экономический, а не дешёвый griefing. Риск низкий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверено и подтверждено как корректное
|
||||||
|
|
||||||
|
- **Подмена singleton-PDA** невозможна: везде сверяется точный адрес и владелец.
|
||||||
|
- **Авторизация**: `update_coef_limit`/`grant_manager_limits` требуют `signer == config.dao_wallet`; `manager_add_ticket` — `signer == allowance.manager_wallet`; `change_ticket_recipient` — `signer == ticket.recipient_wallet`; обновление economy-config — `signer == DAO_AUTHORITY`.
|
||||||
|
- **Ed25519 в `shine_users`**: строгие относительные индексы (−1/−2), `num_signatures == 1`, все три `ix_index == u16::MAX` (данные внутри самой ed25519-инструкции), сверка pubkey/signature/message по хэшу. Подмена и указание на чужую инструкцию исключены.
|
||||||
|
- **Цепочка версий записи** (`version == record_number+1`, `prev_hash == hash(old)`) — корректная защита от replay; сигнатура записи завязана на `root_key`, а не на плательщика.
|
||||||
|
- **Монотонность** `used_bytes`/`last_block_number` и `used_bytes <= paid_limit_bytes`.
|
||||||
|
- **Арифметика**: повсеместные `checked_*`, `overflow-checks = true`, расчёты оракула в `u128` с `u64::try_from` на сужении.
|
||||||
|
- **Оракул Pyth**: пин аккаунта + owner + feed_id + возраст + confidence через официальный SDK.
|
||||||
|
- **Рент-экземпт вольта** сохраняется: `available_vault_lamports` вычитает `minimum_balance`, а суммарная проверка `available >= needed` гарантирует, что после выплат вольт не опустится ниже ренты.
|
||||||
|
- **Двойная оплата тикета** исключена: `is_paid` + инкремент `*_tickets_paid`, следующий шаг адресует следующий индекс.
|
||||||
|
- **Реентранси отсутствует**: CPI только в System Program (transfer/allocate/assign) и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет.
|
||||||
|
- **create_pda_account (новый)**: устойчив к подсеву лампортов; атакующий не может ни выделить данные, ни сменить владельца PDA (нет ключа/seeds), поэтому ветка allocate+assign безопасна.
|
||||||
|
- **shine_login_guard**: stateless, без аккаунтов и средств; DFS-классификация ограничена (`MAX_WORDS_PER_LOGIN = 3`, длина ≤ 20) — без compute-DoS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритет действий
|
||||||
|
|
||||||
|
1. **HIGH** — запретить `recipient == inflow_vault` в `buy_ticket*`, `manager_add_ticket`,
|
||||||
|
`change_ticket_recipient`; добавить `require!(vault.key != recipient.key)` в
|
||||||
|
`transfer_from_vault` как защиту по умолчанию. Закрыть до mainnet.
|
||||||
|
2. **LOW** — зафиксировать правило «получатель ≠ системные PDA» (L1), оценить
|
||||||
|
добавление верхней границы выплаты на шаг (L3).
|
||||||
|
3. **INFO** — формально задокументировать экономику вольта (L4) и known-issue
|
||||||
|
гонки за логином (L5/L2).
|
||||||
|
|
||||||
|
Изменений в код в рамках этого аудита не вносил — это анализ. Готов подготовить патч
|
||||||
|
по пункту 1, если подтвердите.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Аудит безопасности Solana-программ SHiNE — выпуск 3 (12.06.2026)
|
||||||
|
|
||||||
|
Тематический аудит с фокусом на **полноту проверок входных аккаунтов**
|
||||||
|
(signer / owner / каноничный PDA-адрес / system-program / sysvar инструкций /
|
||||||
|
аккаунт оракула) — отвечает на вопрос «точно ли хватает всех проверок входных
|
||||||
|
аккаунтов». Код перечитан целиком после исправлений аудита №2
|
||||||
|
(`Solana-audit-2-by-Claude-11июня2026.md`):
|
||||||
|
|
||||||
|
- `shine_login_guard` (183 строки) — stateless-классификатор логинов, аккаунтами не пользуется;
|
||||||
|
- `shine_users` (1068 строк) — реестр пользователей, PDA-записи, ed25519-подписи, экономика лимитов;
|
||||||
|
- `shine_payments` (1398 строк) — очереди тикетов, выплаты из вольта, оракул Pyth.
|
||||||
|
|
||||||
|
Это ручная (не-Anchor `#[derive(Accounts)]`) реализация на `solana_program`, поэтому
|
||||||
|
каждая проверка аккаунта выполняется явно в коде handler-а. Перебраны: подмена
|
||||||
|
аккаунтов/PDA, подмена владельца, bump-seed атаки, отсутствие signer/authority,
|
||||||
|
подмена system-program и sysvar, подмена аккаунта оракула, неинициализированные/
|
||||||
|
повторно инициализируемые PDA, «лишние» аккаунты.
|
||||||
|
|
||||||
|
## Итоговый вердикт
|
||||||
|
|
||||||
|
**Проверок входных аккаунтов достаточно во всех трёх программах.** По каждому
|
||||||
|
handler присутствуют все требуемые классы проверок; грубых дыр (подмена PDA на
|
||||||
|
чужой аккаунт, отсутствие owner/signer-проверки, использование пользовательского
|
||||||
|
bump, подмена аккаунта оракула) не найдено. Все Critical/HIGH из аудитов №1 и №2
|
||||||
|
закрыты и в этом проходе подтверждены в коде. Новых эксплуатируемых пробелов в
|
||||||
|
валидации аккаунтов нет; есть несколько LOW/INFO-замечаний «by design».
|
||||||
|
|
||||||
|
## Статус прошлых находок (подтверждено в коде на 12.06.2026)
|
||||||
|
|
||||||
|
- 🔴 Critical #1 (economy-config PDA, `shine_users`) — закрыто: `validate_users_economy_config_pda` (адрес + `owner == program_id`) вызывается и в create, и в update перед чтением.
|
||||||
|
- 🔴 Critical #2 (singleton-PDA, `shine_payments`) — закрыто: `validate_singleton_state_pda` (адрес + `owner == id()`) во всех инструкциях.
|
||||||
|
- 🟠 Medium (валидация Pyth) — закрыто: пин адреса `PYTH_SOL_USD_ACCOUNT`, `owner == pyth_receiver`, `PriceUpdateV2`, `feed_id`, возраст, доверительный интервал.
|
||||||
|
- 🟡 Low (griefing на предсказуемых адресах) — закрыто: `create_pda_account` создаёт «поверх предзаполненного» в обеих программах.
|
||||||
|
- 🔴 HIGH аудита №2 (`recipient_wallet == inflow_vault` замораживает выплаты) — закрыто: запрет `recipient == inflow_vault` в `buy_ticket_by_purchase_usd` (стр. 1026), `process_manager_add_ticket` (стр. 747), `process_change_ticket_recipient` (стр. 878) + защита по умолчанию `require!(vault.key != recipient.key)` в `transfer_from_vault` (стр. 1278).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Матрица проверок входных аккаунтов
|
||||||
|
|
||||||
|
### shine_users
|
||||||
|
|
||||||
|
| Инструкция | signer | owner PDA | адрес/seed PDA | system | sysvar / подпись | прочее |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `init_users_economy_config` | ✓ | `owner == system` + `data_is_empty` (анти-reinit) | деривация + сверка | ✓ | — | значения из `settings`, не из ввода |
|
||||||
|
| `update_users_economy_config` | ✓ + `signer == DAO_AUTHORITY` | `owner == program_id` | деривация + сверка | — | — | `lamports_per_limit_step > 0` |
|
||||||
|
| `create_user_pda` | ✓ + `signer == device_key` | user_pda `owner == system` + empty; econ_config `owner == program_id` | user_pda, econ_config, inflow_vault, login_guard — все сверены | ✓ | ed25519 (record sig idx −2, last_block idx −1) | `inflow_vault` сверен с PDA `shine_payments`; login_guard сверен дважды |
|
||||||
|
| `update_user_pda` | ✓ + `signer == device_key` | user_pda `owner == program_id`; econ_config `owner == program_id` | деривация + сверка | ✓ | ed25519 + `version == old+1` + `prev_hash == hash(old)` | immutable-поля сверены с прежней записью |
|
||||||
|
|
||||||
|
### shine_payments
|
||||||
|
|
||||||
|
| Инструкция | signer | owner / валидация PDA | адрес PDA | system | прочее |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `init` | ✓ payer | все 4 PDA `is_uninitialized` | деривация + сверка | ✓ | `dao_wallet` из `settings`, нет лишних аккаунтов |
|
||||||
|
| `update_coef_limit` | ✓ + `signer == config.dao_wallet` | config/coef `owner == id()` | деривация + сверка | — | границы coef/limit/reward; нет лишних аккаунтов |
|
||||||
|
| `grant_manager_limits` | ✓ + `signer == config.dao_wallet` | config `owner == id()`; allowance create/read | allowance из `manager_wallet` | ✓ | `state.manager_wallet == args.manager_wallet` |
|
||||||
|
| `buy_ticket` / `_usd` / `_sol` | ✓ | config/coef/queues `owner == id()` | ticket деривация + сверка + `is_uninitialized` | ✓ | oracle (key+owner+возраст+confidence), `dao_wallet == config.dao_wallet`, `recipient != inflow_vault`, slippage |
|
||||||
|
| `manager_add_ticket` | ✓ | allowance/queues `owner == id()` | allowance из `signer`; ticket деривация + сверка + uninit | ✓ | `allowance.manager_wallet == signer`, `queue_id ∈ {1,2,3}`, `recipient != inflow_vault` |
|
||||||
|
| `step_payout` | ✓ | все singleton-PDA `owner == id()` | ticket деривация + сверка | — | `dao_wallet == config.dao_wallet`, `inflow == config.inflow_vault`, ticket `queue/index/!is_paid/recipient`, oracle |
|
||||||
|
| `change_ticket_recipient` | ✓ + `signer == ticket.recipient_wallet` | queues + ticket `owner == id()` (через `read_state`) | ticket деривация из своих `queue_id/index` + сверка | — | `!is_paid`, запрет менять «следующий к выплате», `recipient != inflow_vault` |
|
||||||
|
|
||||||
|
### shine_login_guard
|
||||||
|
|
||||||
|
Аккаунты не используются (`_accounts`); программа stateless, средствами не владеет.
|
||||||
|
Защита со стороны вызова реализована в `shine_users`: сверяется и адрес вызываемой
|
||||||
|
программы (`login_guard_program.key == SHINE_LOGIN_GUARD_PROGRAM_ID`), и `program_id`
|
||||||
|
в `get_return_data`. Подмена/подделка ответа исключены. Отдельных проверок входных
|
||||||
|
аккаунтов внутри программы не требуется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 LOW / INFO — наблюдения без прямой эксплуатации
|
||||||
|
|
||||||
|
### L1. Permissionless `init` в обеих программах
|
||||||
|
`shine_payments::init` и `shine_users::init_users_economy_config` может вызвать кто
|
||||||
|
угодно первым. Практического эксплойта нет: все значения (включая `dao_wallet` и
|
||||||
|
`DAO_AUTHORITY`) берутся из констант `settings`, а не из ввода, повторная
|
||||||
|
инициализация заблокирована проверками `is_uninitialized` / `data_is_empty`. Риск
|
||||||
|
низкий; при желании привязать init к ожидаемому деплой-кошельку. Совпадает с моделью
|
||||||
|
«первый init = деплой».
|
||||||
|
|
||||||
|
### L2. В `shine_users` нет явной проверки «лишних аккаунтов» — ✅ ИСПРАВЛЕНО (12.06.2026)
|
||||||
|
`shine_payments` в каждом handler делает `require!(account_iter.next().is_none())`.
|
||||||
|
В `shine_users` такой проверки не было — лишние аккаунты в конце списка просто
|
||||||
|
игнорировались (читается строго нужное количество через `next_account_info`). Это
|
||||||
|
безвредно (на безопасность не влияло), но для симметрии и явности добавлено.
|
||||||
|
Класс: гигиена, не уязвимость.
|
||||||
|
|
||||||
|
Закрыто: во все 4 инструкции `shine_users` (`init_users_economy_config`,
|
||||||
|
`update_users_economy_config`, `create_user_pda`, `update_user_pda`) после чтения
|
||||||
|
фиксированного набора аккаунтов добавлено `require!(it.next().is_none(),
|
||||||
|
ShineUsersError::InvalidInstruction)`. Документация — `doc/programs/shine_users.md` §3.4.
|
||||||
|
|
||||||
|
### L3. Гонка за логином (first-come) в `shine_users` — known issue
|
||||||
|
Адрес `user_pda` детерминирован из логина; после закрытия griefing-подсева остаётся
|
||||||
|
обычное состязание за регистрацию (front-run в мемпуле). On-chain решается только
|
||||||
|
commit-reveal; для текущей модели — приемлемый риск, ранее зафиксирован в аудите №2
|
||||||
|
(L2). К проверкам аккаунтов не относится.
|
||||||
|
|
||||||
|
### L4. Экономическая устойчивость вольта (дизайн, не баг)
|
||||||
|
Деньги за покупку тикетов уходят на `dao_wallet`, а выплаты `step_payout` идут из
|
||||||
|
`inflow_vault`, наполняемого регистрационными комиссиями `shine_users` (коэффициент
|
||||||
|
по умолчанию `START_COEF_PPM = 5x`). При недостаточном притоке регистраций вольт
|
||||||
|
истощается и выплаты останавливаются (без потери средств). Это свойство
|
||||||
|
экономической модели «очередь/билеты», а не дефект валидации аккаунтов — отмечено
|
||||||
|
для полноты (ранее L4 в аудите №2). Мониторить баланс вольта vs обязательств.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверено и подтверждено как корректное (по входным аккаунтам)
|
||||||
|
|
||||||
|
- **Подмена PDA** невозможна нигде: всюду пара «деривация `find_program_address` + сверка полного адреса». Пользовательский bump не принимается, `create_program_address` с внешним bump не используется — bump-seed атаки исключены.
|
||||||
|
- **Проверка владельца** при каждом чтении PDA: `read_state` и `validate_singleton_state_pda` (`shine_payments`) требуют `owner == id()`; `validate_users_economy_config_pda` и проверка `user_pda.owner == program_id` (`shine_users`) — перед десериализацией данных.
|
||||||
|
- **Создаваемые PDA**: проверка `is_uninitialized` / `owner == system && data_is_empty` исключает повторную инициализацию и перезапись чужого аккаунта.
|
||||||
|
- **signer / authority**: все handler начинают с обязательного `is_signer`; привилегированные операции дополнительно сверяют ключ с авторитетом (`config.dao_wallet`, `DAO_AUTHORITY`, `allowance.manager_wallet`, `ticket.recipient_wallet`, `device_key`).
|
||||||
|
- **system-program** сверяется с `system_program::ID` там, где идёт создание аккаунта/перевод; **sysvar инструкций** сверяется с `sysvar::instructions::id()` перед ed25519-интроспекцией.
|
||||||
|
- **Аккаунт оракула**: пин адреса `PYTH_SOL_USD_ACCOUNT` + `owner == pyth_receiver` + `feed_id` + возраст (120 с) + доверительный интервал (10%).
|
||||||
|
- **Ed25519 в `shine_users`**: относительные индексы −1/−2, `num_signatures == 1`, все три `ix_index == u16::MAX` (offset-данные внутри самой ed25519-инструкции), сверка `program_id == ed25519_program` и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя.
|
||||||
|
- **Алиасинг аккаунтов**: `recipient != inflow_vault` запрещён на входе во всех точках задания получателя + `vault.key != recipient.key` в `transfer_from_vault`.
|
||||||
|
- **`inflow_vault` в `shine_users`** сверяется с PDA, выведенным из `SHINE_PAYMENTS_PROGRAM_ID` и `SHINE_PAYMENTS_INFLOW_VAULT_SEED` — комиссия не может уйти на чужой адрес.
|
||||||
|
- **Реентранси** отсутствует: CPI только в System Program и в stateless `shine_login_guard` (с проверкой возвращённого `program_id`); обратных вызовов в наши программы нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритет действий
|
||||||
|
|
||||||
|
1. **LOW** — ✅ выполнено 12.06.2026: добавлено `require!(it.next().is_none(), …)` во
|
||||||
|
все инструкции `shine_users` для симметрии с `shine_payments` (L2).
|
||||||
|
2. **INFO** — зафиксировать в эксплуатационной документации known-issue гонки за
|
||||||
|
логином (L3) и экономику вольта (L4); рассмотреть привязку `init` к ожидаемому
|
||||||
|
деплой-кошельку (L1).
|
||||||
|
|
||||||
|
Критичных и высоких находок по полноте проверок входных аккаунтов в этом проходе
|
||||||
|
нет. Единственная LOW-правка (L2) применена в рамках этого же изменения; код
|
||||||
|
`shine_users` собирается успешно (`cargo build -p shine_users`).
|
||||||
@@ -73,7 +73,19 @@ read_sol_usd_price / parse_pyth_price_update_v2 (строки 1038–1075):
|
|||||||
Проверка возраста цены (ORACLE_MAX_AGE_SECS = 120) есть и сделана корректно. Рекомендация: проверять владельца аккаунта, сверять feed_id с константой и валидировать verification_level == Full (или парсить через официальный pyth_solana_receiver_sdk, который уже завендорен в .vendor/).
|
Проверка возраста цены (ORACLE_MAX_AGE_SECS = 120) есть и сделана корректно. Рекомендация: проверять владельца аккаунта, сверять feed_id с константой и валидировать verification_level == Full (или парсить через официальный pyth_solana_receiver_sdk, который уже завендорен в .vendor/).
|
||||||
|
|
||||||
---
|
---
|
||||||
🟡 LOW — DoS через предсказуемые адреса тикетов
|
🟡 LOW — DoS через предсказуемые адреса тикетов — ✅ ИСПРАВЛЕНО (11.06.2026)
|
||||||
|
|
||||||
|
Закрыто: `create_pda_account` в `shine_payments` и `shine_users` переведён на паттерн
|
||||||
|
«создание поверх предзаполненного» (allocate + assign + добор ренты вместо строгого
|
||||||
|
`system_instruction::create_account`). «Подсев» лампортов на заранее известный адрес
|
||||||
|
тикета или пользовательской записи больше не блокирует создание PDA. Проверка
|
||||||
|
`is_uninitialized_account` в payments перестала зависеть от нулевого баланса. Тот же фикс
|
||||||
|
закрывает аналогичный сквоттинг логинов в `shine_users` (адрес выводится из логина).
|
||||||
|
Подробности — в `doc/programs/shine_payments.md` §3.4 и `doc/programs/shine_users.md` §3.3.
|
||||||
|
|
||||||
|
Историческое описание находки ниже.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
is_uninitialized_account (строка 1195) считает аккаунт неинициализированным только если lamports() == 0. Адреса тикетов детерминированы (queue_seed + index), а индекс последователен и предсказуем. Любой может заранее перевести немного лампортов на адрес следующего тикета — тогда create_pda_account упадёт (PdaAlreadyExists / ошибка create_account), заблокировав покупку/добавление тикета. Это griefing-DoS, не кража. Митигировать можно паттерном «create поверх предзаполненного» (allocate + assign + добор ренты) вместо system_instruction::create_account.
|
is_uninitialized_account (строка 1195) считает аккаунт неинициализированным только если lamports() == 0. Адреса тикетов детерминированы (queue_seed + index), а индекс последователен и предсказуем. Любой может заранее перевести немного лампортов на адрес следующего тикета — тогда create_pda_account упадёт (PdaAlreadyExists / ошибка create_account), заблокировав покупку/добавление тикета. Это griefing-DoS, не кража. Митигировать можно паттерном «create поверх предзаполненного» (allocate + assign + добор ренты) вместо system_instruction::create_account.
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,56 @@
|
|||||||
- актуальный IP должен браться через DNS-резолв на момент подключения;
|
- актуальный IP должен браться через DNS-резолв на момент подключения;
|
||||||
- ручное дублирование IP в документации и deploy-скриптах не поддерживать.
|
- ручное дублирование IP в документации и deploy-скриптах не поддерживать.
|
||||||
|
|
||||||
|
## Контуры деплоя
|
||||||
|
|
||||||
|
- Production:
|
||||||
|
- SSH: `player@shineup.me`
|
||||||
|
- Домен: `shineup.me`
|
||||||
|
- IP: `185.229.109.118`
|
||||||
|
- Main test:
|
||||||
|
- SSH: `player@193.8.215.70`
|
||||||
|
- Домен: `test2.shineup.me`
|
||||||
|
- IP: `193.8.215.70`
|
||||||
|
- Reserve test:
|
||||||
|
- SSH: `player@93.170.12.154`
|
||||||
|
- Домен: `test.shineup.me`
|
||||||
|
- IP: `93.170.12.154`
|
||||||
|
|
||||||
## Локальные команды
|
## Локальные команды
|
||||||
|
|
||||||
- Деплой сервера: `./gradlew deployServer`
|
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`
|
||||||
- Деплой UI: `./gradlew deployUI`
|
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`
|
||||||
|
- Production server deploy: `./gradlew deployServerProduction`
|
||||||
|
- Production UI deploy: `./gradlew deployUIProduction`
|
||||||
|
- Reserve test server deploy: `./gradlew deployServerTest`
|
||||||
|
- Reserve test UI deploy: `./gradlew deployUITest`
|
||||||
- Локальный запуск: `./gradlew startLocal`
|
- Локальный запуск: `./gradlew startLocal`
|
||||||
|
|
||||||
|
## Политика подтверждений
|
||||||
|
|
||||||
|
- `shineup.me` — production.
|
||||||
|
- Любые изменения на `shineup.me`, включая deploy сервера, deploy UI, конфиги, перезапуски и миграции, делать только после отдельного явного подтверждения пользователя.
|
||||||
|
- Если пользователь пишет просто `задеплой` без уточнения, по умолчанию это означает deploy на `test2.shineup.me`.
|
||||||
|
|
||||||
|
## Main test deploy (`test2.shineup.me`)
|
||||||
|
|
||||||
|
- Это основной сервер для тестов.
|
||||||
|
- `deployServer` и `deployUI` по умолчанию направлены именно сюда.
|
||||||
|
- Серверный deploy не запускает JUnit/IT-тесты на удалённом сервере.
|
||||||
|
- `deployServer` / `deployServerTest2` делают:
|
||||||
|
- сборку fat-jar локально;
|
||||||
|
- синхронизацию `data/` и `shine.sqlite` с production `shineup.me`;
|
||||||
|
- перенос `application.properties` с production с поправкой `server.ui.indexPath` на `/home/player/SHiNE/shine-ui/index.html`;
|
||||||
|
- установку `systemd` unit на `193.8.215.70`;
|
||||||
|
- перезапуск `shine-server.service`;
|
||||||
|
- установку/проверку Caddy для `test2.shineup.me`.
|
||||||
|
- `deployUI` / `deployUITest2` публикуют UI в `/home/player/SHiNE/shine-ui` на `193.8.215.70`.
|
||||||
|
|
||||||
|
## Reserve test deploy (`test.shineup.me`)
|
||||||
|
|
||||||
|
- `test.shineup.me` пока не использовать для обычного deploy.
|
||||||
|
- Задачи `deployServerTest` и `deployUITest` считаются резервными и требуют отдельной причины.
|
||||||
|
|
||||||
## UI-деплой и Caddy (обязательно)
|
## UI-деплой и Caddy (обязательно)
|
||||||
|
|
||||||
- Целевая директория UI-деплоя: `/home/player/SHiNE/shine-ui`.
|
- Целевая директория UI-деплоя: `/home/player/SHiNE/shine-ui`.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Сервер `193.8.215.70` — основной test (`test2.shineup.me`)
|
||||||
|
|
||||||
|
- Пользователь: `player`
|
||||||
|
- Домен: `test2.shineup.me`
|
||||||
|
- Каталог SHiNE: `/home/player/SHiNE`
|
||||||
|
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
|
||||||
|
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||||
|
- Данные: `/home/player/SHiNE/shine-server/data/`
|
||||||
|
- `shine.sqlite`
|
||||||
|
- `*.bch`
|
||||||
|
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
|
||||||
|
## Сервисы
|
||||||
|
|
||||||
|
- `shine-server.service` (systemd)
|
||||||
|
- `caddy.service` (systemd)
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
- Это основной сервер для тестов SHiNE.
|
||||||
|
- Default deploy по умолчанию должен идти сюда.
|
||||||
|
- Источник данных для тестовой БД: production `shineup.me`.
|
||||||
|
|
||||||
|
## Caddy
|
||||||
|
|
||||||
|
- Конфиг: `/etc/caddy/Caddyfile`
|
||||||
|
- Сайты:
|
||||||
|
- `test2.shineup.me`
|
||||||
|
- `agent.shiningpeople.ru`
|
||||||
|
- Для `test2.shineup.me`:
|
||||||
|
- `root * /home/player/SHiNE/shine-ui`
|
||||||
|
- `try_files {path} /index.html`
|
||||||
|
- `reverse_proxy /ws* -> 127.0.0.1:7070`
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Default server deploy:
|
||||||
|
- `./gradlew deployServer`
|
||||||
|
- `./gradlew deployServerTest2`
|
||||||
|
- Default UI deploy:
|
||||||
|
- `./gradlew deployUI`
|
||||||
|
- `./gradlew deployUITest2`
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
# Сервер `93.170.12.154` — резервный
|
# Сервер `93.170.12.154` — test.shineup.me
|
||||||
|
|
||||||
- Пользователь: `player`
|
- Пользователь: `player`
|
||||||
- Каталог SHiNE: `/home/player/SHiNE`
|
- Каталог SHiNE: `/home/player/SHiNE`
|
||||||
- UI исходник (после rsync): `/home/player/SHiNE/SHiNE-UI`
|
- Домен: `test.shineup.me`
|
||||||
- UI публикация для Caddy: `/var/www/shine-ui`
|
- UI публикация для Caddy: `/home/player/SHiNE/shine-ui`
|
||||||
- Сервер: `/home/player/SHiNE/SHiNE-server/shine-server.jar`
|
- Сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||||
- Данные: `/home/player/SHiNE/SHiNE-server/data/`
|
- Данные: `/home/player/SHiNE/shine-server/data/`
|
||||||
- `shine.sqlite`
|
- `shine.sqlite`
|
||||||
- `*.bch`
|
- `*.bch`
|
||||||
- Логи сервера: `/home/player/SHiNE/SHiNE-server/logs/app.log`
|
- Логи сервера: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||||
|
|
||||||
## Сервисы
|
## Сервисы
|
||||||
|
|
||||||
@@ -17,8 +17,10 @@
|
|||||||
|
|
||||||
## Статус
|
## Статус
|
||||||
|
|
||||||
- Резервный сервер для SHiNE.
|
- Резервный тестовый сервер для SHiNE.
|
||||||
- Основной прод-сервер: `shineup.me` (подключение через `player@shineup.me`, IP определяется через DNS).
|
- Источник данных для тестовой БД: production `shineup.me`.
|
||||||
|
- Пока не использовать для обычного deploy.
|
||||||
|
- Основной прод-сервер: `shineup.me` (`185.229.109.118`).
|
||||||
|
|
||||||
## Caddy
|
## Caddy
|
||||||
|
|
||||||
@@ -26,4 +28,12 @@
|
|||||||
- Настройки:
|
- Настройки:
|
||||||
- `no-store/no-cache` заголовки;
|
- `no-store/no-cache` заголовки;
|
||||||
- `try_files {path} /index.html` (SPA fallback);
|
- `try_files {path} /index.html` (SPA fallback);
|
||||||
- `reverse_proxy /ws* -> 127.0.0.1:7070`.
|
- `reverse_proxy /ws* -> 127.0.0.1:7070`;
|
||||||
|
- целевой сайт: `test.shineup.me`.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Резервные задачи:
|
||||||
|
- `./gradlew deployServerTest`
|
||||||
|
- `./gradlew deployUITest`
|
||||||
|
- Эти задачи пока не использовать без отдельной причины.
|
||||||
|
|||||||
@@ -33,3 +33,15 @@
|
|||||||
- `https://test-solana-tickets.shineup.me`
|
- `https://test-solana-tickets.shineup.me`
|
||||||
- `https://test-solana-tickets.shiningpeople.ru`
|
- `https://test-solana-tickets.shiningpeople.ru`
|
||||||
- Для всех deploy-скриптов и инструкций использовать именно `player@shineup.me`, без жёсткой фиксации IP.
|
- Для всех deploy-скриптов и инструкций использовать именно `player@shineup.me`, без жёсткой фиксации IP.
|
||||||
|
|
||||||
|
## Правило изменений
|
||||||
|
|
||||||
|
- `shineup.me` — production.
|
||||||
|
- Любые изменения на этом сервере делать только после отдельного явного подтверждения пользователя.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Production deploy-задачи:
|
||||||
|
- `./gradlew deployServerProduction`
|
||||||
|
- `./gradlew deployUIProduction`
|
||||||
|
- Default deploy-задачи `./gradlew deployServer` и `./gradlew deployUI` сюда больше не относятся.
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# ESP Pairing и режимы подключения
|
||||||
|
|
||||||
|
Этот документ фиксирует текущие и новые режимы входа/подключения в SHiNE без клиентской UI-реализации. Он нужен как отдельная точка входа по сценариям подключения, чтобы не смешивать обычную авторизацию и серверный pairing через доверенное уже авторизованное устройство пользователя.
|
||||||
|
|
||||||
|
## 1. Текущие режимы
|
||||||
|
|
||||||
|
### 1. Создание новой сессии через `deviceKey`
|
||||||
|
|
||||||
|
Поток:
|
||||||
|
|
||||||
|
`AuthChallenge -> CreateAuthSession`
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
- новое устройство уже владеет приватным `deviceKey`;
|
||||||
|
- сервер проверяет подпись `deviceKey`;
|
||||||
|
- создаётся обычная активная сессия пользователя;
|
||||||
|
- этот поток остаётся без изменений.
|
||||||
|
|
||||||
|
### 2. Повторный вход в существующую сессию через `sessionKey`
|
||||||
|
|
||||||
|
Поток:
|
||||||
|
|
||||||
|
`SessionChallenge -> SessionLogin`
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
- устройство уже владеет приватным `sessionKey`;
|
||||||
|
- сервер проверяет подпись `sessionKey`;
|
||||||
|
- соединение снова входит в существующую сессию;
|
||||||
|
- этот поток тоже остаётся без изменений.
|
||||||
|
|
||||||
|
## 2. Новый режим: добавление сессии через доверенное устройство пользователя
|
||||||
|
|
||||||
|
Новый поток не заменяет обычный логин, а живёт рядом с ним.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
|
||||||
|
- новое устройство знает `login`, а `pairing password` используется только если он включён на доверённом устройстве;
|
||||||
|
- сервер использует пароль только как фильтр от мусора;
|
||||||
|
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
|
||||||
|
- сервер не выдаёт приватные ключи сам от себя.
|
||||||
|
|
||||||
|
Поток версии `v1`:
|
||||||
|
|
||||||
|
1. Любая доверенная сессия пользователя создаёт на сервере pairing-настройку:
|
||||||
|
`UpsertEspPairingSettings`
|
||||||
|
2. Новое устройство создаёт pending-заявку:
|
||||||
|
`StartEspPairing`
|
||||||
|
3. Онлайн доверенная сессия видит список активных заявок:
|
||||||
|
`ListEspPairingRequests`
|
||||||
|
4. Доверенная сессия либо подтверждает заявку:
|
||||||
|
`ApproveEspPairing`
|
||||||
|
5. Либо отклоняет:
|
||||||
|
`RejectEspPairing`
|
||||||
|
6. Новое устройство читает результат:
|
||||||
|
`GetEspPairingStatus`
|
||||||
|
|
||||||
|
## 3. Что именно делает сервер
|
||||||
|
|
||||||
|
- хранит включённость pairing и optional `passwordHash` в формате `sha256$<hex>`;
|
||||||
|
- хранит pairing-заявки всех статусов, но в список активных для доверённого устройства отдаёт только pending `created`;
|
||||||
|
- рассчитывает короткий код `shortCode` из `10` цифр;
|
||||||
|
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
|
||||||
|
- уведомляет онлайн доверенные сессии событием `IncomingEspPairingRequest`, если такие сессии подключены;
|
||||||
|
- хранит переданный `encryptedPayload` как непрозрачную строку и не анализирует его содержимое.
|
||||||
|
|
||||||
|
## 4. Чего сервер в этой версии не делает
|
||||||
|
|
||||||
|
- не передаёт приватный `deviceKey`;
|
||||||
|
- не расшифровывает `encryptedPayload`;
|
||||||
|
- не проверяет криптографию содержимого payload;
|
||||||
|
- не делает клиентский UI;
|
||||||
|
- не навязывает конкретную схему `Ed25519 -> X25519` в коде сервера.
|
||||||
|
|
||||||
|
Это намеренно: серверная версия `v1` подготавливает безопасный каркас маршрутизации и состояния, а настоящая E2E-логика упаковки ключей будет жить на клиентах и ESP-устройствах.
|
||||||
|
|
||||||
|
## 5. Роли и ограничения
|
||||||
|
|
||||||
|
- любая уже авторизованная доверенная сессия пользователя может вызывать:
|
||||||
|
- `UpsertEspPairingSettings`
|
||||||
|
- `ListEspPairingRequests`
|
||||||
|
- `ApproveEspPairing`
|
||||||
|
- `RejectEspPairing`
|
||||||
|
- новое устройство может вызвать `StartEspPairing` и `GetEspPairingStatus` без уже существующей авторизованной сессии;
|
||||||
|
- `payloadType` поддерживается в вариантах:
|
||||||
|
- `1` — минимальный пакет
|
||||||
|
- `2` — расширенный пакет
|
||||||
|
- `3` — полный пакет
|
||||||
|
|
||||||
|
Сервер не интерпретирует эти три типа глубже, а только фиксирует их в состоянии заявки.
|
||||||
|
|
||||||
|
## 6. Статусы pairing-заявки
|
||||||
|
|
||||||
|
- `created` — заявка создана и ждёт решения доверенной сессии;
|
||||||
|
- `approved` — доверенная сессия подтвердила и приложила `encryptedPayload`;
|
||||||
|
- `rejected` — доверенная сессия отклонила заявку;
|
||||||
|
- `expired` — TTL заявки истёк до подтверждения.
|
||||||
|
|
||||||
|
## 7. Практический смысл
|
||||||
|
|
||||||
|
Эта схема даёт нужное разделение доверия:
|
||||||
|
|
||||||
|
- пароль на сервере, если он включён, только отсеивает лишних;
|
||||||
|
- онлайн доверенная сессия решает, добавлять ли новую сессию;
|
||||||
|
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
|
||||||
|
|
||||||
|
Текущий формат pairing-пароля:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||||||
|
```
|
||||||
@@ -34,7 +34,7 @@ ls -l /dev/ttyACM0
|
|||||||
|
|
||||||
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||||
- `original-firmware/` — backup/restore заводской прошивки
|
- `original-firmware/` — backup/restore заводской прошивки
|
||||||
- `test-device/` — прошивки и `burn.sh`
|
- `main-device/` — прошивки и `burn.sh`
|
||||||
- `reference/` — заметки и ссылки
|
- `reference/` — заметки и ссылки
|
||||||
|
|
||||||
## 4) Бэкап перед любыми экспериментами
|
## 4) Бэкап перед любыми экспериментами
|
||||||
@@ -59,7 +59,7 @@ cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
|||||||
Главный скрипт:
|
Главный скрипт:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ESP32-S3-Touch-AMOLED-2.16/test-device
|
cd ESP32-S3-Touch-AMOLED-2.16/main-device
|
||||||
./burn.sh <mode>
|
./burn.sh <mode>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ ls -l /dev/ttyACM0
|
|||||||
|
|
||||||
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||||
- `original-firmware/` — backup/restore заводской прошивки
|
- `original-firmware/` — backup/restore заводской прошивки
|
||||||
- `test-device/` — прошивки и `burn.sh`
|
- `main-device/` — прошивки и `burn.sh`
|
||||||
- `reference/` — заметки и ссылки
|
- `reference/` — заметки и ссылки
|
||||||
|
|
||||||
## 4) Бэкап перед любыми экспериментами
|
## 4) Бэкап перед любыми экспериментами
|
||||||
@@ -59,7 +59,7 @@ cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
|||||||
Главный скрипт:
|
Главный скрипт:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ESP32-S3-Touch-AMOLED-2.16/test-device
|
cd ESP32-S3-Touch-AMOLED-2.16/main-device
|
||||||
./burn.sh <mode>
|
./burn.sh <mode>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
|
|
||||||
- `official-demo/` — официальный репозиторий примеров Waveshare
|
- `official-demo/` — официальный репозиторий примеров Waveshare
|
||||||
- `original-firmware/` — резервная копия заводской прошивки
|
- `original-firmware/` — резервная копия заводской прошивки
|
||||||
- `test-device/` — скрипты быстрой проверки устройства
|
- `main-device/` — скрипты быстрой проверки устройства и основной скетч `shine_homeserver_main/`
|
||||||
- `reference/` — локальные заметки по документации и железу
|
- `reference/` — локальные заметки по документации и железу
|
||||||
|
- `main-device/shine_homeserver_main/` — основной рабочий скетч ESP32-проекта `SHiNE`
|
||||||
|
|
||||||
Примечание по git:
|
Примечание по git:
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@
|
|||||||
1. Сделать backup текущей прошивки:
|
1. Сделать backup текущей прошивки:
|
||||||
- `cd original-firmware && ./backup_factory.sh`
|
- `cd original-firmware && ./backup_factory.sh`
|
||||||
2. Залить тест экрана/тача:
|
2. Залить тест экрана/тача:
|
||||||
- `cd ../test-device && ./burn.sh widgets`
|
- `cd ../main-device && ./burn.sh widgets`
|
||||||
3. Залить тест динамика:
|
3. Залить тест динамика:
|
||||||
- `cd ../test-device && ./burn.sh audio`
|
- `cd ../main-device && ./burn.sh audio`
|
||||||
|
4. Залить основной UI:
|
||||||
|
- `cd ../main-device && ./burn.sh shine-homeserver-main`
|
||||||
|
|||||||
+17
-7
@@ -1,6 +1,10 @@
|
|||||||
# Test Device
|
# Main Device
|
||||||
|
|
||||||
Скрипт заливает официальные Arduino-примеры для быстрой проверки платы.
|
Основной скетч homeserver и старые тестовые скетчи для быстрой проверки платы.
|
||||||
|
`burn.sh` теперь:
|
||||||
|
- сам пытается найти USB-порт ESP32;
|
||||||
|
- сначала делает быструю инкрементальную сборку;
|
||||||
|
- если быстрая сборка не удалась, автоматически повторяет полную `clean`-сборку.
|
||||||
|
|
||||||
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.
|
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.
|
||||||
|
|
||||||
@@ -10,7 +14,10 @@
|
|||||||
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
|
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
|
||||||
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
|
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
|
||||||
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
|
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
|
||||||
- `subserver-ui` — основной UI-прототип сабсервера SHiNE: NVS, PIN, Wi-Fi, серверы, кошелёк, QR, запросы
|
- `homeserver-ui` — совместимый алиас, указывает на `shine_homeserver_main/`
|
||||||
|
- `shine-homeserver-main` — основной скетч проекта `SHiNE` для ESP32, текущая рабочая версия UI
|
||||||
|
- `shine-homeserver-ui-main` — старое имя основного скетча, оставлено как совместимый алиас
|
||||||
|
- `legacy-homeserver-ui` — старый UI-прототип `shine_homeserver_ui/`, оставлен как тестовый и не является основным
|
||||||
- `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями
|
- `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями
|
||||||
- `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/`
|
- `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/`
|
||||||
- `gfx-layout-test` — тест геометрии и нижних рядов кнопок
|
- `gfx-layout-test` — тест геометрии и нижних рядов кнопок
|
||||||
@@ -18,9 +25,9 @@
|
|||||||
- `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке
|
- `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке
|
||||||
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
|
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
|
||||||
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
|
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
|
||||||
- `lvgl-subserver-touch-test` — гибрид: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из `shine_subserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
|
- `lvgl-subserver-touch-test` — старый гибридный тест: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из старого `shine_homeserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
|
||||||
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
|
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
|
||||||
- `lvgl-nav-minimal-test` — новый минимальный UI-каркас сабсервера: `HOME`, `SETTINGS_MENU`, `Wi-Fi`, `Server`, `Account`, свайпы, крупные кнопки и реальная настройка Wi-Fi с сохранением в NVS
|
- `lvgl-nav-minimal-test` — старое имя основного скетча, теперь ведёт на `shine_homeserver_main/` для совместимости
|
||||||
|
|
||||||
Запуск:
|
Запуск:
|
||||||
|
|
||||||
@@ -28,7 +35,10 @@
|
|||||||
- `./burn.sh audio`
|
- `./burn.sh audio`
|
||||||
- `./burn.sh hello`
|
- `./burn.sh hello`
|
||||||
- `./burn.sh simple`
|
- `./burn.sh simple`
|
||||||
- `./burn.sh subserver-ui`
|
- `./burn.sh homeserver-ui`
|
||||||
|
- `./burn.sh shine-homeserver-main`
|
||||||
|
- `./burn.sh shine-homeserver-ui-main`
|
||||||
|
- `./burn.sh legacy-homeserver-ui`
|
||||||
- `./burn.sh text-test`
|
- `./burn.sh text-test`
|
||||||
- `./burn.sh gfx-text-test`
|
- `./burn.sh gfx-text-test`
|
||||||
- `./burn.sh gfx-layout-test`
|
- `./burn.sh gfx-layout-test`
|
||||||
@@ -39,4 +49,4 @@
|
|||||||
- `./burn.sh lvgl-subserver-touch-test`
|
- `./burn.sh lvgl-subserver-touch-test`
|
||||||
- `./burn.sh lvgl-russian-font-test`
|
- `./burn.sh lvgl-russian-font-test`
|
||||||
- `./burn.sh lvgl-nav-minimal-test`
|
- `./burn.sh lvgl-nav-minimal-test`
|
||||||
- `./flash_shine_subserver_ui.sh` - автоматически находит USB-порт и заливает `shine_subserver_ui`
|
- `./flash_shine_homeserver_main.sh` - автоматически находит USB-порт и заливает `shine_homeserver_main`
|
||||||
+48
-14
@@ -5,18 +5,39 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
|
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
|
||||||
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
|
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
|
||||||
MODE="${1:-widgets}"
|
MODE="${1:-widgets}"
|
||||||
PORT="${PORT:-/dev/ttyACM0}"
|
PORT="${PORT:-}"
|
||||||
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
|
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
|
||||||
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
|
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
|
||||||
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
|
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
|
||||||
|
|
||||||
|
detect_port() {
|
||||||
|
local detected
|
||||||
|
detected="$(arduino-cli board list 2>/dev/null | awk '/\/dev\/tty(ACM|USB)/ {print $1; exit}')"
|
||||||
|
if [[ -n "${detected}" ]]; then
|
||||||
|
echo "${detected}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for candidate in /dev/ttyACM* /dev/ttyUSB*; do
|
||||||
|
if [[ -e "${candidate}" ]]; then
|
||||||
|
echo "${candidate}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
case "${MODE}" in
|
case "${MODE}" in
|
||||||
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
|
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
|
||||||
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
|
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
|
||||||
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
|
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
|
||||||
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
|
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
|
||||||
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
|
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
|
||||||
subserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_subserver_ui" ;;
|
homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
shine-homeserver-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
shine-homeserver-ui-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
|
legacy-homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_ui" ;;
|
||||||
text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;;
|
text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;;
|
||||||
gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;;
|
gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;;
|
||||||
gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;;
|
gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;;
|
||||||
@@ -26,14 +47,21 @@ case "${MODE}" in
|
|||||||
lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;;
|
lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;;
|
||||||
lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;;
|
lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;;
|
||||||
lvgl-russian-font-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_russian_font_test" ;;
|
lvgl-russian-font-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_russian_font_test" ;;
|
||||||
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_nav_minimal_test" ;;
|
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown mode: ${MODE}" >&2
|
echo "Unknown mode: ${MODE}" >&2
|
||||||
echo "Use one of: hello, widgets, audio, simple, argon2, subserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test, lvgl-nav-minimal-test" >&2
|
echo "Use one of: hello, widgets, audio, simple, argon2, homeserver-ui, shine-homeserver-main, shine-homeserver-ui-main, legacy-homeserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test" >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ -z "${PORT}" ]]; then
|
||||||
|
if ! PORT="$(detect_port)"; then
|
||||||
|
echo "Failed to auto-detect ESP32 port. Set PORT=/dev/ttyACM0 ./burn.sh ${MODE}" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "== Mode: ${MODE}"
|
echo "== Mode: ${MODE}"
|
||||||
echo "== Sketch: ${SKETCH_DIR}"
|
echo "== Sketch: ${SKETCH_DIR}"
|
||||||
echo "== Port: ${PORT}"
|
echo "== Port: ${PORT}"
|
||||||
@@ -41,17 +69,23 @@ echo "== FQBN: ${FQBN}"
|
|||||||
|
|
||||||
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
|
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
|
||||||
|
|
||||||
arduino-cli compile \
|
compile_args=(
|
||||||
--clean \
|
--fqbn "${FQBN}"
|
||||||
--fqbn "${FQBN}" \
|
--build-path "${BUILD_DIR}"
|
||||||
--build-path "${BUILD_DIR}" \
|
--output-dir "${OUT_DIR}"
|
||||||
--output-dir "${OUT_DIR}" \
|
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino"
|
||||||
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino" \
|
--library "${DEMO_BASE}/libraries/SensorLib"
|
||||||
--library "${DEMO_BASE}/libraries/SensorLib" \
|
--library "${DEMO_BASE}/libraries/XPowersLib"
|
||||||
--library "${DEMO_BASE}/libraries/XPowersLib" \
|
--library "${DEMO_BASE}/libraries/lvgl"
|
||||||
--library "${DEMO_BASE}/libraries/lvgl" \
|
--library "${DEMO_BASE}/libraries/Mylibrary"
|
||||||
--library "${DEMO_BASE}/libraries/Mylibrary" \
|
|
||||||
"${SKETCH_DIR}"
|
"${SKETCH_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "== Compile: fast incremental build"
|
||||||
|
if ! arduino-cli compile "${compile_args[@]}"; then
|
||||||
|
echo "== Compile: fast build failed, retrying clean build"
|
||||||
|
arduino-cli compile --clean "${compile_args[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
arduino-cli upload \
|
arduino-cli upload \
|
||||||
-p "${PORT}" \
|
-p "${PORT}" \
|
||||||
+2
-2
@@ -43,9 +43,9 @@ fi
|
|||||||
if [[ -z "${PORT}" ]]; then
|
if [[ -z "${PORT}" ]]; then
|
||||||
echo "Не удалось автоматически найти USB-порт ESP32." >&2
|
echo "Не удалось автоматически найти USB-порт ESP32." >&2
|
||||||
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
|
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
|
||||||
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_subserver_ui.sh" >&2
|
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_homeserver_main.sh" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== Найден порт: ${PORT}"
|
echo "== Найден порт: ${PORT}"
|
||||||
PORT="${PORT}" "${ROOT_DIR}/burn.sh" subserver-ui
|
PORT="${PORT}" "${ROOT_DIR}/burn.sh" shine-homeserver-main
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# SHiNE Homeserver UI Main
|
||||||
|
|
||||||
|
Это основной рабочий скетч ESP32-проекта `SHiNE`.
|
||||||
|
|
||||||
|
Текущая каноническая точка запуска:
|
||||||
|
|
||||||
|
- `./burn.sh shine-homeserver-main`
|
||||||
|
- `./burn.sh homeserver-ui`
|
||||||
|
|
||||||
|
Историческое имя этого скетча:
|
||||||
|
|
||||||
|
- `lvgl-nav-minimal-test`
|
||||||
|
|
||||||
|
Прежние тестовые варианты для этой платы остаются в `main-device/test_sketches/` и должны восприниматься как старые диагностические сборки, а не как основной UI.
|
||||||
+6196
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
|||||||
|
# SHiNE Homeserver UI Legacy
|
||||||
|
|
||||||
|
Это старый тестовый вариант UI для ESP32-платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||||
|
|
||||||
|
Не использовать как основной скетч проекта.
|
||||||
|
Основной рабочий скетч сейчас лежит в `../shine_homeserver_main/`.
|
||||||
+21
-21
@@ -97,7 +97,7 @@ enum ActionId {
|
|||||||
ACT_VERIFY_SERVERS,
|
ACT_VERIFY_SERVERS,
|
||||||
ACT_SET_TEST_SERVERS,
|
ACT_SET_TEST_SERVERS,
|
||||||
ACT_EDIT_LOGIN,
|
ACT_EDIT_LOGIN,
|
||||||
ACT_EDIT_SUBSERVER,
|
ACT_EDIT_HOMESERVER,
|
||||||
ACT_GENERATE_SECRET,
|
ACT_GENERATE_SECRET,
|
||||||
ACT_CLEAR_ACCOUNT,
|
ACT_CLEAR_ACCOUNT,
|
||||||
ACT_SHOW_QR,
|
ACT_SHOW_QR,
|
||||||
@@ -137,7 +137,7 @@ enum EditTarget {
|
|||||||
EDIT_SSID,
|
EDIT_SSID,
|
||||||
EDIT_WIFI_PASSWORD,
|
EDIT_WIFI_PASSWORD,
|
||||||
EDIT_LOGIN,
|
EDIT_LOGIN,
|
||||||
EDIT_SUBSERVER,
|
EDIT_HOMESERVER,
|
||||||
EDIT_API,
|
EDIT_API,
|
||||||
EDIT_RPC,
|
EDIT_RPC,
|
||||||
EDIT_WS,
|
EDIT_WS,
|
||||||
@@ -174,7 +174,7 @@ struct AppData {
|
|||||||
String wifiSsid;
|
String wifiSsid;
|
||||||
String wifiPassword;
|
String wifiPassword;
|
||||||
String login;
|
String login;
|
||||||
String subserverName;
|
String homeserverName;
|
||||||
String secret;
|
String secret;
|
||||||
String walletAddress;
|
String walletAddress;
|
||||||
String userPdaAddress;
|
String userPdaAddress;
|
||||||
@@ -551,7 +551,7 @@ static bool canRegister() {
|
|||||||
|
|
||||||
static String registrationSummary() {
|
static String registrationSummary() {
|
||||||
if (gData.registered) {
|
if (gData.registered) {
|
||||||
return "Сабсервер активен";
|
return "Homeserver активен";
|
||||||
}
|
}
|
||||||
if (!gData.wifiReady) {
|
if (!gData.wifiReady) {
|
||||||
return "Нужен Wi-Fi";
|
return "Нужен Wi-Fi";
|
||||||
@@ -1179,7 +1179,7 @@ static bool awaitTransactionConfirmation(const String &signatureB58, String &mes
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool registerSubserverOnSolana(String &messageOut) {
|
static bool registerHomeserverOnSolana(String &messageOut) {
|
||||||
messageOut = "";
|
messageOut = "";
|
||||||
if (!gDerivedKeys.ready) {
|
if (!gDerivedKeys.ready) {
|
||||||
if (!restoreDerivedKeysFromSecret()) {
|
if (!restoreDerivedKeysFromSecret()) {
|
||||||
@@ -1656,7 +1656,7 @@ static bool refreshWalletBalance(String &messageOut) {
|
|||||||
static void seedRequests() {
|
static void seedRequests() {
|
||||||
gRequests[0].type = "Вход в сессию";
|
gRequests[0].type = "Вход в сессию";
|
||||||
gRequests[0].actor = "Chrome / aidarkc";
|
gRequests[0].actor = "Chrome / aidarkc";
|
||||||
gRequests[0].details = "Клиент просит подключиться к сабсерверу и открыть сессию без ввода пароля.";
|
gRequests[0].details = "Клиент просит подключиться к homeserverу и открыть сессию без ввода пароля.";
|
||||||
gRequests[0].status = "Ожидает";
|
gRequests[0].status = "Ожидает";
|
||||||
|
|
||||||
gRequests[1].type = "Подпись сообщения";
|
gRequests[1].type = "Подпись сообщения";
|
||||||
@@ -1670,7 +1670,7 @@ static void loadDefaults() {
|
|||||||
gData.wifiSsid = "";
|
gData.wifiSsid = "";
|
||||||
gData.wifiPassword = "";
|
gData.wifiPassword = "";
|
||||||
gData.login = "";
|
gData.login = "";
|
||||||
gData.subserverName = "subserver1";
|
gData.homeserverName = "homeserver1";
|
||||||
gData.secret = "";
|
gData.secret = "";
|
||||||
gData.walletAddress = "";
|
gData.walletAddress = "";
|
||||||
gData.userPdaAddress = "";
|
gData.userPdaAddress = "";
|
||||||
@@ -1692,7 +1692,7 @@ static void saveData() {
|
|||||||
gPrefs.putString("wifi_ssid", gData.wifiSsid);
|
gPrefs.putString("wifi_ssid", gData.wifiSsid);
|
||||||
gPrefs.putString("wifi_pass", gData.wifiPassword);
|
gPrefs.putString("wifi_pass", gData.wifiPassword);
|
||||||
gPrefs.putString("login", gData.login);
|
gPrefs.putString("login", gData.login);
|
||||||
gPrefs.putString("subserver", gData.subserverName);
|
gPrefs.putString("homeserver", gData.homeserverName);
|
||||||
gPrefs.putString("secret", gData.secret);
|
gPrefs.putString("secret", gData.secret);
|
||||||
gPrefs.putString("wallet", gData.walletAddress);
|
gPrefs.putString("wallet", gData.walletAddress);
|
||||||
gPrefs.putString("user_pda", gData.userPdaAddress);
|
gPrefs.putString("user_pda", gData.userPdaAddress);
|
||||||
@@ -1714,7 +1714,7 @@ static void loadData() {
|
|||||||
gData.wifiSsid = gPrefs.getString("wifi_ssid", gData.wifiSsid);
|
gData.wifiSsid = gPrefs.getString("wifi_ssid", gData.wifiSsid);
|
||||||
gData.wifiPassword = gPrefs.getString("wifi_pass", gData.wifiPassword);
|
gData.wifiPassword = gPrefs.getString("wifi_pass", gData.wifiPassword);
|
||||||
gData.login = gPrefs.getString("login", gData.login);
|
gData.login = gPrefs.getString("login", gData.login);
|
||||||
gData.subserverName = gPrefs.getString("subserver", gData.subserverName);
|
gData.homeserverName = gPrefs.getString("homeserver", gData.homeserverName);
|
||||||
gData.secret = gPrefs.getString("secret", gData.secret);
|
gData.secret = gPrefs.getString("secret", gData.secret);
|
||||||
gData.walletAddress = gPrefs.getString("wallet", gData.walletAddress);
|
gData.walletAddress = gPrefs.getString("wallet", gData.walletAddress);
|
||||||
gData.userPdaAddress = gPrefs.getString("user_pda", gData.userPdaAddress);
|
gData.userPdaAddress = gPrefs.getString("user_pda", gData.userPdaAddress);
|
||||||
@@ -1758,8 +1758,8 @@ static void generateSecretAndWallet() {
|
|||||||
gData.registrationSignature = "";
|
gData.registrationSignature = "";
|
||||||
gData.registered = false;
|
gData.registered = false;
|
||||||
gData.online = false;
|
gData.online = false;
|
||||||
if (gData.subserverName.length() == 0) {
|
if (gData.homeserverName.length() == 0) {
|
||||||
gData.subserverName = "subserver1";
|
gData.homeserverName = "homeserver1";
|
||||||
}
|
}
|
||||||
saveData();
|
saveData();
|
||||||
}
|
}
|
||||||
@@ -1815,7 +1815,7 @@ static String editTargetLabel() {
|
|||||||
case EDIT_SSID: return "SSID";
|
case EDIT_SSID: return "SSID";
|
||||||
case EDIT_WIFI_PASSWORD: return "Пароль Wi-Fi";
|
case EDIT_WIFI_PASSWORD: return "Пароль Wi-Fi";
|
||||||
case EDIT_LOGIN: return "Логин";
|
case EDIT_LOGIN: return "Логин";
|
||||||
case EDIT_SUBSERVER: return "Имя сабсервера";
|
case EDIT_HOMESERVER: return "Имя homeserver";
|
||||||
case EDIT_API: return "API URL";
|
case EDIT_API: return "API URL";
|
||||||
case EDIT_RPC: return "RPC URL";
|
case EDIT_RPC: return "RPC URL";
|
||||||
case EDIT_WS: return "WS URL";
|
case EDIT_WS: return "WS URL";
|
||||||
@@ -1846,7 +1846,7 @@ static void drawHomeScreen() {
|
|||||||
drawPanel(20, 92, 440, 98, C_PANEL, C_BORDER, 16);
|
drawPanel(20, 92, 440, 98, C_PANEL, C_BORDER, 16);
|
||||||
drawText(36, 122, registrationSummary(), canRegister() || gData.registered ? C_ACCENT : C_WARN, (const uint8_t *)FONT_HEAD);
|
drawText(36, 122, registrationSummary(), canRegister() || gData.registered ? C_ACCENT : C_WARN, (const uint8_t *)FONT_HEAD);
|
||||||
drawText(36, 152, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT, (const uint8_t *)FONT_BODY);
|
drawText(36, 152, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT, (const uint8_t *)FONT_BODY);
|
||||||
drawText(36, 174, "Сабсервер: " + gData.subserverName, C_MUTE, (const uint8_t *)FONT_BODY);
|
drawText(36, 174, "Homeserver: " + gData.homeserverName, C_MUTE, (const uint8_t *)FONT_BODY);
|
||||||
|
|
||||||
drawPanel(20, 204, 210, 82, C_CARD, C_BORDER, 12);
|
drawPanel(20, 204, 210, 82, C_CARD, C_BORDER, 12);
|
||||||
drawText(34, 232, "Wi-Fi", C_TEXT, (const uint8_t *)FONT_BODY);
|
drawText(34, 232, "Wi-Fi", C_TEXT, (const uint8_t *)FONT_BODY);
|
||||||
@@ -1871,7 +1871,7 @@ static void drawStatusScreen() {
|
|||||||
drawTopBar("Статус");
|
drawTopBar("Статус");
|
||||||
drawPanel(20, 92, 440, 286, C_PANEL, C_BORDER, 16);
|
drawPanel(20, 92, 440, 286, C_PANEL, C_BORDER, 16);
|
||||||
drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
|
drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
|
||||||
drawText(36, 148, "Сабсервер: " + gData.subserverName, C_TEXT);
|
drawText(36, 148, "Homeserver: " + gData.homeserverName, C_TEXT);
|
||||||
drawText(36, 174, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
|
drawText(36, 174, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
|
||||||
drawText(36, 200, "Отпечаток: " + (gData.secretReady ? shortenValue(gData.secret) : "-"), C_MUTE, (const uint8_t *)FONT_SMALL);
|
drawText(36, 200, "Отпечаток: " + (gData.secretReady ? shortenValue(gData.secret) : "-"), C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||||
drawText(36, 226, "Wi-Fi: " + boolText(gData.wifiReady, "готов", "не готов"), gData.wifiReady ? C_ACCENT : C_WARN);
|
drawText(36, 226, "Wi-Fi: " + boolText(gData.wifiReady, "готов", "не готов"), gData.wifiReady ? C_ACCENT : C_WARN);
|
||||||
@@ -1947,13 +1947,13 @@ static void drawAccountScreen() {
|
|||||||
drawTopBar("Аккаунт");
|
drawTopBar("Аккаунт");
|
||||||
drawPanel(20, 92, 440, 188, C_PANEL, C_BORDER, 16);
|
drawPanel(20, 92, 440, 188, C_PANEL, C_BORDER, 16);
|
||||||
drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
|
drawText(36, 122, "Логин: " + (gData.login.length() ? gData.login : "не задан"), C_TEXT);
|
||||||
drawText(36, 152, "Сабсервер: " + gData.subserverName, C_TEXT);
|
drawText(36, 152, "Homeserver: " + gData.homeserverName, C_TEXT);
|
||||||
drawText(36, 182, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
|
drawText(36, 182, "Секрет: " + boolText(gData.secretReady, "сохранён", "не задан"), gData.secretReady ? C_ACCENT : C_WARN);
|
||||||
drawText(36, 212, "Кошелёк: " + (gData.walletAddress.length() ? shortenValue(gData.walletAddress, 10, 8) : "не создан"), C_MUTE, (const uint8_t *)FONT_SMALL);
|
drawText(36, 212, "Кошелёк: " + (gData.walletAddress.length() ? shortenValue(gData.walletAddress, 10, 8) : "не создан"), C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||||
drawText(36, 236, "Регистрация: " + boolText(gData.registered, "выполнена", "не выполнена"), gData.registered ? C_ACCENT : C_WARN);
|
drawText(36, 236, "Регистрация: " + boolText(gData.registered, "выполнена", "не выполнена"), gData.registered ? C_ACCENT : C_WARN);
|
||||||
drawText(36, 260, "PDA: " + registrationDetailsShort(), C_MUTE, (const uint8_t *)FONT_SMALL);
|
drawText(36, 260, "PDA: " + registrationDetailsShort(), C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||||
addButton(20, 300, 212, 48, ACT_EDIT_LOGIN, "Изменить логин");
|
addButton(20, 300, 212, 48, ACT_EDIT_LOGIN, "Изменить логин");
|
||||||
addButton(248, 300, 212, 48, ACT_EDIT_SUBSERVER, "Имя сабсервера");
|
addButton(248, 300, 212, 48, ACT_EDIT_HOMESERVER, "Имя homeserver");
|
||||||
addButton(20, 360, 212, 48, ACT_GENERATE_SECRET, "Сгенерировать", true, C_OK);
|
addButton(20, 360, 212, 48, ACT_GENERATE_SECRET, "Сгенерировать", true, C_OK);
|
||||||
addButton(248, 360, 212, 48, ACT_CLEAR_ACCOUNT, "Очистить", true, C_BUTTON2);
|
addButton(248, 360, 212, 48, ACT_CLEAR_ACCOUNT, "Очистить", true, C_BUTTON2);
|
||||||
addButton(20, 420, 440, 36, ACT_BACK, "Назад");
|
addButton(20, 420, 440, 36, ACT_BACK, "Назад");
|
||||||
@@ -2193,9 +2193,9 @@ static void applyEditValue() {
|
|||||||
gData.registrationSignature = "";
|
gData.registrationSignature = "";
|
||||||
gNotice = "Логин сохранён";
|
gNotice = "Логин сохранён";
|
||||||
break;
|
break;
|
||||||
case EDIT_SUBSERVER:
|
case EDIT_HOMESERVER:
|
||||||
gData.subserverName = value.length() ? value : "subserver1";
|
gData.homeserverName = value.length() ? value : "homeserver1";
|
||||||
gNotice = "Имя сабсервера сохранено";
|
gNotice = "Имя homeserver сохранено";
|
||||||
break;
|
break;
|
||||||
case EDIT_API:
|
case EDIT_API:
|
||||||
gData.apiUrl = value;
|
gData.apiUrl = value;
|
||||||
@@ -2351,7 +2351,7 @@ static void handleAction(ActionId action) {
|
|||||||
}
|
}
|
||||||
if (action == ACT_CONFIRM_YES) {
|
if (action == ACT_CONFIRM_YES) {
|
||||||
if (gConfirmTarget == CONFIRM_REGISTER) {
|
if (gConfirmTarget == CONFIRM_REGISTER) {
|
||||||
registerSubserverOnSolana(gNotice);
|
registerHomeserverOnSolana(gNotice);
|
||||||
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
|
} else if (gConfirmTarget == CONFIRM_CLEAR_ACCOUNT) {
|
||||||
gData.secret = "";
|
gData.secret = "";
|
||||||
gData.walletAddress = "";
|
gData.walletAddress = "";
|
||||||
@@ -2445,7 +2445,7 @@ static void handleAction(ActionId action) {
|
|||||||
gNeedRedraw = true;
|
gNeedRedraw = true;
|
||||||
break;
|
break;
|
||||||
case ACT_EDIT_LOGIN: openEdit(EDIT_LOGIN, gData.login, false); break;
|
case ACT_EDIT_LOGIN: openEdit(EDIT_LOGIN, gData.login, false); break;
|
||||||
case ACT_EDIT_SUBSERVER: openEdit(EDIT_SUBSERVER, gData.subserverName, false); break;
|
case ACT_EDIT_HOMESERVER: openEdit(EDIT_HOMESERVER, gData.homeserverName, false); break;
|
||||||
case ACT_GENERATE_SECRET:
|
case ACT_GENERATE_SECRET:
|
||||||
generateSecretAndWallet();
|
generateSecretAndWallet();
|
||||||
gNotice = gData.secretReady ? "Секрет сгенерирован, device-кошелёк выведен из него" : "Не удалось сгенерировать секрет";
|
gNotice = gData.secretReady ? "Секрет сгенерирован, device-кошелёк выведен из него" : "Не удалось сгенерировать секрет";
|
||||||
+5
-5
@@ -1,8 +1,9 @@
|
|||||||
# Test Sketches
|
# Test Sketches
|
||||||
|
|
||||||
Набор отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
Набор старых отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||||
|
|
||||||
Скетчи в этой папке нужны для быстрой проверки конкретных гипотез без влияния на основной `shine_subserver_ui`.
|
Скетчи в этой папке нужны для быстрой проверки конкретных гипотез и не являются основным UI проекта.
|
||||||
|
Основной скетч сейчас лежит в `main-device/shine_homeserver_main/`.
|
||||||
|
|
||||||
## Список
|
## Список
|
||||||
|
|
||||||
@@ -12,9 +13,9 @@
|
|||||||
- `lvgl_interaction_test/` - расширенный тест `LVGL` с 9 кнопками, touch-вводом и статусом нажатия
|
- `lvgl_interaction_test/` - расширенный тест `LVGL` с 9 кнопками, touch-вводом и статусом нажатия
|
||||||
- `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL`
|
- `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL`
|
||||||
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
|
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
|
||||||
- `lvgl_subserver_touch_test/` - гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из `shine_subserver_ui`; подтверждён на реальном устройстве
|
- `lvgl_subserver_touch_test/` - старый гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из старого `shine_homeserver_ui`; подтверждён на реальном устройстве
|
||||||
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
|
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
|
||||||
- `lvgl_nav_minimal_test/` - новый минимальный навигационный каркас сабсервера на рабочем `LVGL + subserver touch`, расширенный настройкой Wi-Fi и сохранением в NVS
|
- `lvgl_nav_minimal_test/` - старое тестовое имя, этот скетч перенесён в `shine_homeserver_main/` и теперь является основным
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
@@ -28,4 +29,3 @@
|
|||||||
- `./burn.sh lvgl-official-based-test`
|
- `./burn.sh lvgl-official-based-test`
|
||||||
- `./burn.sh lvgl-subserver-touch-test`
|
- `./burn.sh lvgl-subserver-touch-test`
|
||||||
- `./burn.sh lvgl-russian-font-test`
|
- `./burn.sh lvgl-russian-font-test`
|
||||||
- `./burn.sh lvgl-nav-minimal-test`
|
|
||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
#include <Arduino_GFX_Library.h>
|
#include <Arduino_GFX_Library.h>
|
||||||
#include <TouchDrvCSTXXX.hpp>
|
#include <TouchDrvCSTXXX.hpp>
|
||||||
|
|
||||||
// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_subserver_ui.
|
// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_homeserver_ui.
|
||||||
|
|
||||||
#define PIN_LCD_CS 12
|
#define PIN_LCD_CS 12
|
||||||
#define PIN_LCD_SCLK 38
|
#define PIN_LCD_SCLK 38
|
||||||
@@ -146,7 +146,7 @@ static void createUi() {
|
|||||||
lv_obj_align(gVersionLabel, LV_ALIGN_TOP_MID, 0, 12);
|
lv_obj_align(gVersionLabel, LV_ALIGN_TOP_MID, 0, 12);
|
||||||
|
|
||||||
lv_obj_t *subtitle = lv_label_create(screen);
|
lv_obj_t *subtitle = lv_label_create(screen);
|
||||||
lv_label_set_text(subtitle, "Touch path comes from shine_subserver_ui. Tap buttons and watch status.");
|
lv_label_set_text(subtitle, "Touch path comes from shine_homeserver_ui. Tap buttons and watch status.");
|
||||||
lv_obj_set_width(subtitle, 436);
|
lv_obj_set_width(subtitle, 436);
|
||||||
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
|
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
|
||||||
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);
|
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user