Обновить UI чатов и связей и добавить документацию ИТХ

This commit is contained in:
AidarKC
2026-07-14 13:39:01 +04:00
parent af12d7b954
commit 4cb98f7ca0
8 changed files with 535 additions and 22 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.2.310
server.version=1.2.289
client.version=1.2.311
server.version=1.2.290
+12
View File
@@ -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:
+5
View File
@@ -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)`.
+81
View File
@@ -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:0006: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`.
@@ -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:0006: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: <dayIndex>` (теги информационные, доверия к ним нет — истина в подписи).
### 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
<dayIndex>
<dayDate>
<closedBy>
<prevCheckpointHashHex | ->
<prevArweaveTxId | ->
<stateRootHex>
<bundleRootHex>
<closedAtMs>
```
- `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 <dayIndex> <checkpointHashHex> <arweaveTxId>
```
- Согласие другого сервера — `REACTION_LIKE`, таргетирующий блок этого поста.
Отзыв согласия — `REACTION_UNLIKE`. Комментарии не требуются.
- Смысл: человекочитаемое зеркало + криптографическая проверяемость через Arweave
(пост и лайки подписаны ключами серверов, зарегистрированными в Solana), с лагом в
один день (попадут в следующий бандл).
- Машинный якорь «здесь и сейчас» — PDA-слоты; канал вторичен.
- При реализации обязательно дописать команду `/.checkpoint` в
`docs/Blockchain/02_Channel_Commands.md`.
## 8. Алгоритм дежурного (закрытие дня D)
1. В окне `D+1 00:0006: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` — фиксировать изменения.
+54 -1
View File
@@ -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';
-11
View File
@@ -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');
+63 -8
View File
@@ -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);
}