diff --git a/VERSION.properties b/VERSION.properties index 37c58aa..de7e598 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.310 -server.version=1.2.289 +client.version=1.2.311 +server.version=1.2.290 diff --git a/docs/Blockchain/CHANGELOG.md b/docs/Blockchain/CHANGELOG.md index ad73912..466c9a5 100644 --- a/docs/Blockchain/CHANGELOG.md +++ b/docs/Blockchain/CHANGELOG.md @@ -1,5 +1,17 @@ # История изменений документации блокчейна +## 2026-07-13 14:04:59 +0400 +- Базовый коммит-ориентир: `af12d7b`. +- Добавлена новая папка документации `docs/ИТХ/` — ежедневное закрытие блокчейна (чекпоинты): + - `docs/ИТХ/README.md` — краткий обзор для людей; + - `docs/ИТХ/Спецификация_ИТХ_v1.md` — подробная спецификация для реализации + (формат чекпоинта в Arweave, меркл-правила ИТХ, rota PDA DAO, слот `CheckpointStateBlock` + в PDA серверов, канал закрытий с командой `/.checkpoint`, алгоритмы дежурного и проверяющего). +- Формат блоков и правила `AddBlock` не изменены: это новый мета-слой поверх существующего блокчейна. +- Код пока не реализован; при реализации обязательно дописать `/.checkpoint` + в `02_Channel_Commands.md` и `CheckpointStateBlock` в формат `user_pda`. +- В `README.md` этого каталога добавлен раздел «Смежная документация» со ссылками на `docs/ИТХ/`. + # 2026-06-26 17:45:18 +0400 - Базовый коммит-ориентир: `44a1ba0`. - На `t.shineup.me` подтверждена рабочая схема startup sync и full-resync: diff --git a/docs/Blockchain/README.md b/docs/Blockchain/README.md index 39587f7..302df73 100644 --- a/docs/Blockchain/README.md +++ b/docs/Blockchain/README.md @@ -24,6 +24,11 @@ 10. [CHANGELOG.md](./CHANGELOG.md) Журнал изменений документации. +## Смежная документация +- [../ИТХ/README.md](../ИТХ/README.md) — ежедневное закрытие блокчейна (ИТХ): краткий обзор. +- [../ИТХ/Спецификация_ИТХ_v1.md](../ИТХ/Спецификация_ИТХ_v1.md) — точная спецификация чекпоинтов (Arweave/Solana/канал закрытий). +- [sync-between-servers.md](./sync-between-servers.md) — живая межсерверная синхронизация блоков и DM. + ## Важные ограничения MVP - Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI. - Поддерживаемый рабочий сценарий UI на текущем этапе: `stories (type=0)` и `public (type=1)`. diff --git a/docs/ИТХ/README.md b/docs/ИТХ/README.md new file mode 100644 index 0000000..1e1227e --- /dev/null +++ b/docs/ИТХ/README.md @@ -0,0 +1,81 @@ +# ИТХ — ежедневное закрытие блокчейна SHiNE (кратко, для людей) + +Статус: дизайн согласован, код не написан. +Подробная спецификация для реализации: [Спецификация_ИТХ_v1.md](./Спецификация_ИТХ_v1.md). + +## Что это + +Раз в сутки один из серверов SHiNE («дежурный») делает **закрытие дня**: + +1. Строит меркл-дерево по вершинам всех пользовательских цепочек — + его корень называется **ИТХ** (итоговый хэш дня). +2. Собирает **бандл** — все новые блоки, появившиеся с прошлого закрытия, + с хэшами и подписями пользователей. +3. Заливает всё это одним файлом (**чекпоинт**) в Arweave. + Каждый чекпоинт ссылается на предыдущий — получается цепочка чекпоинтов. +4. Пишет в свой Solana PDA: «крайний чекпоинт — вот такой хэш, лежит по такому Arweave-адресу». +5. Публикует пост о закрытии в своём канале закрытий прямо в Сиянии. + +Остальные серверы проверяют чекпоинт, и если всё сходится: + +- обновляют слот в **своём** PDA на этот же чекпоинт («согласен»); +- ставят **лайк** на пост дежурного в канале. + +Несогласие = просто ничего не делать (слот остаётся на прошлом чекпоинте). + +## Зачем + +- **Неизменяемость истории.** Прошлые дни зафиксированы в Arweave и Solana — + их нельзя переписать даже сговором всех живых серверов. +- **Независимая проверяемость.** Любой желающий может скачать цепочку чекпоинтов + из Arweave и проверить всю историю, не доверяя ни одному живому серверу. +- **Резерв последней надежды.** Даже если все серверы погибнут, + история до последнего чекпоинта не потеряна. + +В повседневной работе серверы синхронизируются друг с другом напрямую — +Arweave почти не читается, это гарантия, а не рабочая лошадка. + +## Кто дежурит + +График ведёт DAO в специальном PDA: упорядоченный список логинов серверов +и дата начала эпохи. Дежурный дня = `dayIndex % количество_серверов`. +Обновлять список может только DAO. + +## Расписание суток (UTC) + +| Время | Что происходит | +| --- | --- | +| День D, 00:00–24:00 | Обычная работа, блоки копятся. | +| D+1, 00:00–06:00 | Дежурный закрывает день D: Arweave → PDA → пост в канал. | +| D+1, до 24:00 | Остальные проверяют и подтверждают (PDA + лайк). | + +Если дежурный не закрыл (или закрыл неверно) — день просто пропускается: +никто ничего не подтверждает, а следующий дежурный включает в свой бандл +блоки за все пропущенные дни и ссылается на последний признанный чекпоинт. + +## Как понять, какой чекпоинт «настоящий» + +**Актуальный чекпоинт = последний, на который указывают слоты +больше половины серверов из графика.** Кворума on-chain нет — +каждый сервер публикует только своё мнение, а наблюдатель считает голоса сам. +Сервер, который систематически ошибается, DAO просто исключает из списка. + +## Восстановление сервера с нуля + +1. Прочитать DAO PDA → список серверов графика. +2. Прочитать их PDA-слоты → последний признанный чекпоинт (>половины голосов). +3. Скачать из Arweave цепочку чекпоинтов от генезиса и раскатать бандлы. +4. «Хвост» после последнего чекпоинта добрать живой синхронизацией с серверами. + +## Кто платит + +Каждый сервер платит сам: за свои Arweave-загрузки в свои дежурные дни +и за свои Solana-транзакции. + +## Связанные документы + +- [Спецификация_ИТХ_v1.md](./Спецификация_ИТХ_v1.md) — точный формат и алгоритмы (для реализации). +- `docs/Blockchain/` — формат блоков и цепочек SHiNE. +- `docs/Blockchain/sync-between-servers.md` — живая межсерверная синхронизация. +- `docs/Solana_Architecture/README.md` — устройство Solana-части. +- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — формат `user_pda`. diff --git a/docs/ИТХ/Спецификация_ИТХ_v1.md b/docs/ИТХ/Спецификация_ИТХ_v1.md new file mode 100644 index 0000000..4b82094 --- /dev/null +++ b/docs/ИТХ/Спецификация_ИТХ_v1.md @@ -0,0 +1,318 @@ +# Спецификация ИТХ v1 — ежедневное закрытие блокчейна SHiNE + +Статус: дизайн согласован пользователем, реализации в коде нет. +Краткое описание для людей: [README.md](./README.md). + +Документ рассчитан на то, чтобы по нему можно было написать код без дополнительных +уточнений. Все байтовые порядки — big-endian (BE), все хэши — SHA-256 (32 байта), +все подписи — Ed25519, все времена — UTC. + +--- + +## 1. Термины + +| Термин | Значение | +| --- | --- | +| **День D** | Календарные сутки UTC `[D 00:00:00; D+1 00:00:00)`. | +| **dayIndex** | Порядковый номер суток от эпохи графика: `dayIndex = (началоСуток_D − epochStart) / 86400_000`. У `epochStart` dayIndex = 0. | +| **Дежурный** | Сервер, закрывающий день D: `rota.servers[dayIndex mod rota.servers.length]`. | +| **Чекпоинт** | Артефакт закрытия дня: контейнер (заголовок + state-список + бандл), одним файлом в Arweave. | +| **ИТХ** | Итоговый хэш дня = `stateRoot` — меркл-корень по вершинам всех цепочек. | +| **Бандл** | Все блоки, не покрытые предыдущим признанным чекпоинтом. | +| **checkpointHash** | SHA-256 канонического preimage заголовка (раздел 5.3). Идентификатор чекпоинта. | +| **Признанный чекпоинт** | Последний чекпоинт, на который указывают PDA-слоты больше половины серверов графика. | + +## 2. График дежурств (rota PDA, DAO) + +Отдельный PDA, создаётся и обновляется **только DAO authority** +(точная реализация — в Solana-модуле `shine-solana/shine/`, отдельным решением; +предлагаемые seeds: `["checkpoint_rota"]`). + +Логическое содержимое: + +``` +RotaV1 +- version: u8 = 1 +- epoch_start_utc_day: u32 // число суток от unix-эпохи (00:00 UTC) +- servers_count: u8 +- servers: [ login_len: u8, login: UTF-8 ] * servers_count // упорядоченный список +``` + +Правила: + +- Дежурный дня D определяется по состоянию rota PDA на момент `00:00 UTC дня D+1` + (момент начала окна закрытия). Изменение списка не пересчитывает прошлые дни. +- При изменении состава серверов будущее распределение сдвигается автоматически — + это допустимо, календарь «дата → сервер» не ведётся. + +## 3. Расписание суток (UTC) + +| Окно | Действие | +| --- | --- | +| `D+1 00:00 – 06:00` | Дежурный строит чекпоинт дня D, заливает в Arweave, обновляет свой PDA-слот (`role=closed`), публикует пост в канале закрытий. | +| `D+1 06:00 – 24:00` | Остальные серверы проверяют; при успехе — свой PDA-слот (`role=agreed`) + лайк на пост. Проверять можно и раньше 06:00, как только чекпоинт появился. | + +- Нет валидного чекпоинта дня D к началу следующего окна закрытия → день D **пропущен**: + никто не меняет свои слоты; дежурный дня D+1 включает блоки за все непокрытые дни + и ссылается `prev`-полями на последний признанный чекпоинт. +- Сервер, отстававший несколько дней, догоняет **последовательно**: проверяет цепочку + чекпоинтов по одному (каждый — относительно предыдущего) и в конце ставит свой слот + на последний проверенный. Опоздавшее согласие допустимо в любой момент. + +## 4. Что попадает в бандл (правило среза) + +- Срез = момент начала сборки чекпоинта дежурным (внутри окна `00:00–06:00`). +- В бандл входят все блоки, известные дежурному на момент среза и **не покрытые + предыдущим признанным чекпоинтом**: для каждой цепочки `c` — блоки с номера + `prevState[c].lastNumber + 1` до локальной вершины; новые цепочки — целиком, + с первого блока (номер `0`). +- Блоки, созданные уже утром D+1 до среза, допустимо включать — это не ошибка. +- Полнота относительно других серверов **не требуется**: блоки, которых у дежурного + не было, войдут в следующий чекпоинт. Проверяющие не считают «у меня есть больше» + причиной несогласия. + +## 5. Формат чекпоинта (один файл / одна Arweave-транзакция) + +### 5.1. Контейнер + +``` +magic: 8 байт ASCII "SHNCKPT1" +headerLen: u32 BE — длина headerJson в байтах +headerJson: UTF-8 JSON (раздел 5.2) +bundle: записи бандла подряд (раздел 5.4), суммарной длиной до конца файла +``` + +Рекомендуемые Arweave-теги: `App-Name: SHiNE`, `Type: checkpoint`, +`Day-Index: ` (теги информационные, доверия к ним нет — истина в подписи). + +### 5.2. headerJson + +```json +{ + "v": 1, + "dayIndex": 123, + "dayDate": "2026-07-12", + "closedBy": "serverlogin", + "closedAtMs": 1789200000000, + "serverPubKeyB58": "…", + "prevCheckpointHashHex": "…64 hex… | null", + "prevArweaveTxId": "… | null", + "stateRootHex": "…64 hex…", + "stateCount": 3500, + "bundleRootHex": "…64 hex…", + "bundleCount": 4200, + "state": [ + { "name": "alice-001", "lastNumber": 17, "lastHashHex": "…64 hex…" } + ], + "signatureB64": "…" +} +``` + +- `state` отсортирован по `name` **байтово по UTF-8, по возрастанию**, без дублей. +- `dayDate` — дата дня D (для читаемости; проверяется соответствие `dayIndex`). +- Для генезис-чекпоинта `prevCheckpointHashHex` и `prevArweaveTxId` равны `null`, + `prevState` считается пустым, бандл содержит всю историю. +- Все hex — в нижнем регистре. + +### 5.3. Канонический preimage, checkpointHash и подпись + +Preimage — UTF-8 строка из 9 строк, разделитель `\n` (без завершающего `\n`; +`null`-значения кодируются как `-`): + +``` +SHiNE-CHECKPOINT-V1 + + + + + + + + +``` + +- `checkpointHash = SHA-256(preimage)`. +- `signatureB64 = Ed25519(preimage)` актуальным клиентским ключом дежурного сервера + (ключ из его `user_pda`, действующий на момент закрытия; `serverPubKeyB58` его дублирует). +- `checkpointHash` не зависит от форматирования JSON — только от полей preimage. + +### 5.4. Запись бандла + +Записи идут подряд, отсортированы по `(name байтово, blockNumber)` по возрастанию: + +``` +nameLen: u16 BE +name: UTF-8 (blockchainName) +blockNumber: u64 BE +blockLen: u32 BE +blockBytes: полный блок как в хранилище: preimage + sigMarker + signature64 +``` + +`bundleCount` в заголовке = число записей; парсер обязан проверить, что записи +заканчиваются ровно на конце файла. + +### 5.5. Меркл-правила (общие для state и бандла) + +- Лист: `SHA-256(0x00 || данные листа)`. +- Узел: `SHA-256(0x01 || left32 || right32)`. +- При нечётном числе узлов на уровне последний **поднимается без изменения**. +- Пустое дерево → 32 нулевых байта. + +Данные листа: + +- **state** (порядок листьев = порядок `state`): + `nameBytes || 0x00 || lastNumber u64 BE || lastHash32`. + Корень = `stateRoot` = **ИТХ**. +- **бандл** (порядок листьев = порядок записей): полные байты записи из 5.4 + (включая nameLen/name/blockNumber/blockLen). Корень = `bundleRoot`. + +## 6. Слот чекпоинта в PDA сервера + +Новый типизированный блок в `user_pda` сервера (рядом с `ServerProfileBlock (30)`; +финальный номер `block_type` и правки `shine-user-pda-format-v.1.0.md` — +при реализации Solana-части, отдельным решением). Предложение: + +``` +CheckpointStateBlock (block_type = 60, block_version = 0) +- day_index: u32 +- checkpoint_hash: [32] +- arweave_tx_len: u8, arweave_tx: ASCII +- closed_by_len: u8, closed_by: UTF-8 +- my_role: u8 // 1 = closed (я закрывал), 2 = agreed (я согласен) +- updated_at_ms: u64 +``` + +Правила: + +- Слот **один**, перезаписывается на месте; истории в PDA нет + (история — цепочка чекпоинтов в Arweave и посты в канале). +- Дежурный после заливки ставит свой чекпоинт с `my_role=1`. +- Проверивший и согласный сервер ставит тот же `(day_index, checkpoint_hash, + arweave_tx, closed_by)` с `my_role=2`. +- **Несогласие = не трогать слот.** Отдельной записи «против» нет. + +Правило признания (для наблюдателей и восстановления): + +> Актуальный чекпоинт = последний (максимальный `day_index`), на который указывают +> одинаковые `(day_index, checkpoint_hash)` в слотах **больше половины** серверов +> из rota PDA. Рекомендация клиентам: до достижения половины считать актуальным +> предыдущий признанный. + +## 7. Канал закрытий в Сиянии + +- У каждого сервера графика — свой публичный канал `type=1`, slug `checkpoints` + (создаётся обычным `TECH_CREATE_CHANNEL`). +- При закрытии дежурный пишет в свой канал `TEXT_POST`: + + ``` + /.checkpoint + ``` + +- Согласие другого сервера — `REACTION_LIKE`, таргетирующий блок этого поста. + Отзыв согласия — `REACTION_UNLIKE`. Комментарии не требуются. +- Смысл: человекочитаемое зеркало + криптографическая проверяемость через Arweave + (пост и лайки подписаны ключами серверов, зарегистрированными в Solana), с лагом в + один день (попадут в следующий бандл). +- Машинный якорь «здесь и сейчас» — PDA-слоты; канал вторичен. +- При реализации обязательно дописать команду `/.checkpoint` в + `docs/Blockchain/02_Channel_Commands.md`. + +## 8. Алгоритм дежурного (закрытие дня D) + +1. В окне `D+1 00:00–06:00` убедиться по rota PDA, что дежурный — я. +2. Определить предыдущий признанный чекпоинт (свой слот + слоты остальных, раздел 6); + загрузить его `state` (локальный кэш или Arweave). Для генезиса — пустой. +3. Зафиксировать срез: для каждой цепочки — локальная вершина `(lastNumber, lastHash)`. +4. Собрать бандл по разделу 4, отсортировать по 5.4, посчитать `bundleRoot`. +5. Сформировать `state` по срезу, отсортировать, посчитать `stateRoot` (ИТХ). +6. Собрать preimage, `checkpointHash`, подписать серверным ключом. +7. Залить контейнер одной Arweave-транзакцией (JWK сервера с хоста, вне git), + дождаться подтверждения, получить `arweaveTxId`. +8. Обновить свой PDA-слот: `(dayIndex, checkpointHash, arweaveTxId, closedBy=я, role=1)`. +9. Опубликовать пост `/.checkpoint …` в своём канале закрытий. +10. Любая ошибка на шагах 7–9 → повторять до конца окна; не успел — день пропущен, + ничего не публиковать частично (PDA-слот и пост ставить только после успешного Arweave). + +## 9. Алгоритм проверяющего + +Вход: обнаружен новый чекпоинт дня D от сервера S (по его PDA-слоту и/или посту). + +1. **График**: `S == rota[dayIndex mod N]` по состоянию rota PDA на `00:00 D+1`; + `dayIndex` больше `day_index` последнего признанного локально чекпоинта. +2. Скачать Arweave-транзакцию из слота S; проверить `magic`, длины, парсинг. +3. Пересчитать preimage → `checkpointHash`; сверить со слотом S (и постом, если есть). +4. Проверить `signatureB64` ключом S (актуальный клиентский ключ из `user_pda` S; + допускается ключ, действовавший в день закрытия — см. TODO про ротацию). +5. `prevCheckpointHashHex`/`prevArweaveTxId` == локально признанный крайний чекпоинт. + Иначе — отказ. +6. Пересчитать `bundleRoot` по записям; сверить. Проверить сортировку и `bundleCount`. +7. **Мостик prev→state** (полнота бандла): для каждой цепочки из `state` + блоки бандла образуют непрерывную последовательность номеров и `prevHash`-ссылок + от `prevState[c]` (или от блока 0 для новой цепочки) ровно до `state[c]`. + Для каждого блока: `hash32 == SHA-256(preimage)`, структура валидна по правилам + `docs/Blockchain/01_Common_Block_Format.md`, подпись Ed25519 валидна ключом + владельца цепочки, действующим на день закрытия. +8. Пересчитать `stateRoot` по `state`; сверить (это и есть проверка ИТХ). +9. **Конфликты с локальными данными**: если локально для `(цепочка, номер)` есть блок + с другим `hash32` — отказ (v1 форки автоматически не разруливает; см. TODO). + «У меня есть блоки, которых нет в бандле» — НЕ отказ. +10. Импортировать недостающие блоки бандла в локальное хранилище тем же валидационным + путём, что и межсерверная синхронизация (`sync-between-servers.md`). +11. Успех → обновить свой PDA-слот (`role=2`) и поставить лайк на пост S. + Отказ → не делать ничего on-chain; подробно залогировать причину + (рекомендуемый префикс лога: `CHECKPOINT_REJECT`). + +Проверка детерминирована: два честных сервера с любым набором локальных блоков +приходят к одному вердикту, кроме шага 9 (локальный конфликт), который сигналит +о реальном форке пользовательской цепочки. + +## 10. Восстановление сервера с нуля + +1. Прочитать rota PDA → список серверов. +2. Прочитать их PDA-слоты → признанный чекпоинт (правило раздела 6). +3. От него по `prevArweaveTxId` спуститься до генезиса, затем раскатать бандлы + от генезиса вверх, валидируя каждый чекпоинт по разделу 9 (шаги 2–8). +4. Хвост после последнего чекпоинта добрать живой синхронизацией с серверами. + +## 11. Стоимость и ключи + +- Каждый сервер платит сам: Arweave — за свои дежурные дни, Solana — за свои слоты. +- Arweave JWK сервера хранится только на хосте (например, + `/home/player/SHiNE/secrets/…`), путь задаётся конфигом; в git не попадает — + по той же схеме, что `test.freeAvatar.walletJwkPath`. + +## 12. Ориентир для реализации на сервере (не нормативно) + +- `CheckpointScheduler` — таймер окон 00:00/06:00 UTC, определение роли по rota PDA. +- `CheckpointBuilder` — срез, бандл, меркл, preimage, подпись (разделы 4–5, 8). +- `ArweaveCheckpointUploader` — заливка контейнера, ожидание подтверждения. +- `CheckpointVerifier` — раздел 9; переиспользовать валидацию блоков из `AddBlock`/sync. +- `CheckpointSolanaSlot` — чтение/запись `CheckpointStateBlock`, чтение rota PDA. +- `CheckpointChannelPublisher` — пост `/.checkpoint` и лайки через обычный `AddBlock`. +- Конфиги (предложение): `checkpoint.enabled`, `checkpoint.arweave.jwkPath`, + `checkpoint.rota.pda` (или деривация по seeds), `checkpoint.closeWindowHours=6`. +- Локально хранить: `state` последнего признанного чекпоинта + его + `(dayIndex, hash, arweaveTxId)` — чтобы не ходить в Arweave на каждое закрытие. + +## 13. TODO / сознательно отложено + +- **История ключей при ротации**: если сервер/пользователь меняет ключ несколько раз + за день, для проверки старых подписей нужна история версий ключей. v1 живёт с + «ключ, актуальный на день закрытия»; при внедрении ротации — хранить версии. +- **Разруливание форков** пользовательских цепочек (шаг 9.9): правило «наиболее + длинная валидная цепочка» + детерминированный tie-break. v1 — отказ и лог. +- **Автопропуск штрафника** в ротации (2 подряд закрытия без единого согласия). +- **On-chain кворум** вместо правила чтения «>половины» — если понадобится. +- **Сжатие бандла** (gzip) и **чанкование** очень больших бандлов на несколько + Arweave-транзакций с манифестом. v1 — один несжатый файл. +- Экономия Solana-транзакций: считать согласия только по лайкам канала — возможно + после обкатки v1. + +## 14. Связанные документы (обновлять при реализации) + +- `docs/Blockchain/01_Common_Block_Format.md` — формат блока (используется в 5.4, 9.7). +- `docs/Blockchain/02_Channel_Commands.md` — дописать `/.checkpoint`. +- `docs/Blockchain/sync-between-servers.md` — путь импорта блоков (9.10). +- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md` — дописать + `CheckpointStateBlock` и rota PDA при реализации Solana-части. +- `docs/Blockchain/CHANGELOG.md` — фиксировать изменения. diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 4adb160..153839c 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -406,6 +406,31 @@ function formatMessageTime(valueMs) { }).format(new Date(timeMs)); } +function formatCallDurationCompact(totalSeconds) { + const total = Math.max(0, Math.floor(Number(totalSeconds || 0))); + const hours = Math.floor(total / 3600); + const minutes = Math.floor((total % 3600) / 60); + const seconds = total % 60; + if (hours > 0) { + return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + } + return `${minutes}:${String(seconds).padStart(2, '0')}`; +} + +function buildCallStatusText(callSummary) { + if (!callSummary) return ''; + if (callSummary.status === 'completed') { + return `Длительность: ${formatCallDurationCompact(callSummary.durationSec)}`; + } + const reason = String(callSummary.reason || '').trim().toLowerCase(); + if (reason === 'offline') return 'Не дозвонился: абонент не в сети'; + if (reason === 'no_answer') return 'Не дозвонился: нет ответа'; + if (reason === 'connect_failed') return 'Не дозвонился: не удалось установить соединение'; + if (reason === 'busy') return 'Не дозвонился: абонент занят'; + if (reason === 'declined') return 'Не дозвонился: звонок отклонён'; + return 'Не дозвонился'; +} + function resolveDeliveryStatus(msg) { if (msg?.from !== 'out') return ''; if (msg?.secondTick) return '✓✓'; @@ -503,6 +528,32 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode const bubbleKind = String(msg?.kind || autoKind || '').trim(); bubble.className = `bubble ${msg.from}${bubbleKind ? ` ${bubbleKind}` : ''}`; + if (parsedText.callSummary) { + const callCard = document.createElement('div'); + callCard.className = 'bubble-call-card'; + + const callHead = document.createElement('div'); + callHead.className = 'bubble-call-head'; + + const callIcon = document.createElement('span'); + callIcon.className = 'bubble-call-icon'; + callIcon.textContent = '☎'; + + const callTitle = document.createElement('span'); + callTitle.className = 'bubble-call-title'; + callTitle.textContent = msg?.from === 'out' ? 'Исходящий звонок' : 'Входящий звонок'; + + callHead.append(callIcon, callTitle); + + const callMetaLine = document.createElement('div'); + callMetaLine.className = 'bubble-call-line bubble-call-line--muted'; + const callStatus = buildCallStatusText(parsedText.callSummary); + callMetaLine.textContent = callStatus; + + callCard.append(callHead, callMetaLine); + bubble.append(callCard); + } + const textNode = document.createElement('div'); textNode.className = 'bubble-text'; textNode.textContent = parsedText.displayText || ''; @@ -533,7 +584,9 @@ function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode bubble.append(replyBox); } } - bubble.append(textNode); + if (!parsedText.callSummary) { + bubble.append(textNode); + } const metaNode = document.createElement('div'); metaNode.className = 'bubble-meta'; diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index 6c71171..b76c113 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -268,16 +268,6 @@ export function render({ navigate, route }) { return makeProfileRoute(cleanLogin); } - function helpText() { - return [ - 'Обозначения на экране связей:', - '• Синие линии — близкие друзья.', - '• Оранжевые линии — родственники.', - '• Стрелка на линии — односторонняя связь.', - '• Если стрелки нет, связь взаимная.', - ].join('\n'); - } - function persistHistory() { persistedCenterLogin = centerLogin; persistedCenterHistory = [...centerHistory]; @@ -523,7 +513,6 @@ export function render({ navigate, route }) { }, rightActions: [ { label: 'Найти', onClick: openSearchModal }, - { label: '?', onClick: () => window.alert(helpText()) }, ], }); const backBtnEl = header.querySelector('.header-left .icon-btn'); diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index c95ec84..aa4ab4b 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -1062,11 +1062,52 @@ .bubble.call-tech { max-width: 90%; justify-self: center; - border-radius: 10px; - border: 1px solid rgba(131, 162, 223, 0.32); - background: rgba(40, 55, 84, 0.55); + border-radius: 16px; + border: 1px solid rgba(132, 170, 231, 0.28); + background: rgba(24, 36, 58, 0.72); color: #dce8ff; font-size: 12px; + padding: 12px 14px; +} + +.bubble-call-card { + display: grid; + gap: 4px; + min-width: min(280px, 72vw); +} + +.bubble-call-head { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.bubble-call-icon { + display: inline-grid; + place-items: center; + width: 28px; + height: 28px; + border-radius: 999px; + background: rgba(80, 170, 255, 0.18); + border: 1px solid rgba(123, 191, 255, 0.28); + color: #d9ecff; + font-size: 15px; +} + +.bubble-call-title { + font-size: 14px; + font-weight: 700; + color: #eef6ff; +} + +.bubble-call-line { + font-size: 12px; + line-height: 1.35; + color: rgba(230, 240, 255, 0.94); +} + +.bubble-call-line--muted { + color: rgba(188, 207, 238, 0.78); } .chat-input { @@ -1523,17 +1564,21 @@ textarea.input { .network-header-overlay { position: sticky; - top: max(8px, env(safe-area-inset-top)); + top: max(14px, calc(env(safe-area-inset-top) + 8px)); left: max(8px, env(safe-area-inset-left)); right: max(8px, env(safe-area-inset-right)); margin-bottom: 0; z-index: 12; pointer-events: none; + padding: 9px 12px 0; } .network-header-overlay.page-header { + display: grid; + grid-template-columns: minmax(42px, 1fr) auto minmax(58px, 1fr); + align-items: center; margin-bottom: 0; - gap: 6px; + gap: 10px; } .network-header-overlay .header-left, @@ -1542,20 +1587,30 @@ textarea.input { gap: 6px; } +.network-header-overlay .header-left { + justify-content: flex-start; +} + +.network-header-overlay .header-actions { + justify-content: flex-end; +} + .network-header-overlay .icon-btn { pointer-events: auto; - min-height: 30px; - padding: 5px 8px; - font-size: 11px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; border-radius: 10px; } .network-header-overlay .page-title { font-size: 16px; + line-height: 32px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + text-align: center; text-shadow: 0 1px 8px rgba(0, 0, 0, 0.45); }