Этап 5: история — экспорт и импорт .bare, устройства, место, пагинация

Экспорт: ключ архива из секрета аккаунта через HKDF, заголовок ровно 65 байт
(magic, версия, соль, 32 сырых байта отпечатка владельца, iv) и он же целиком
AAD шифротекста. Ника владельца в файле нет. Импорт сверяет отпечаток до
расшифровки, сливает идемпотентно по id, а записи peers берёт только для
ников, которых в локальном TOFU ещё нет: архивом доверие к ключу не перебить.

Настройки: устройства с датой и пометкой «это устройство», «занято N МБ»,
кнопка «экспортировать» в подтверждении выхода и в подтверждении входа
под другим ником — долг этапа 1 и обещание ADR-029 закрыты.
Лента: страницы по 50 с подгрузкой вверх без прыжка прокрутки; новая
страница вставляется, а не пересобирает ленту.

ADR-050: импорт не перезаписывает лежащую запись — у своей есть состояние
отправки, которого в архиве нет.
ADR-054: архив — недоверенный ввод. Ревью собрало архивы с ts вне диапазона
Date, мусорным lastId, ником с bidi-переопределением и roomId с обходом пути:
каждый из них навсегда ломал ленту или счётчик. Теперь форма ника, roomId,
id, ts и автора проверяется, а lastId из файла не читается вовсе.
ADR-051, 052, 053: тексты и кнопки, устройства и место, страницы ленты
без виртуализации.

Приёмка: формат сверен побайтно на модулях, скачанных с боевого сервера, —
смещения заголовка, отпечаток сырыми байтами, новые соль и iv на каждый
экспорт, подмена любого байта заголовка ломает расшифровку, чужой секрет
не открывает, семь видов битых файлов отвергнуты.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
2026-08-23 02:57:51 +03:00
co-authored by Claude Opus 5
parent 8f67f4aa4d
commit 0c878477d2
21 changed files with 1274 additions and 53 deletions
@@ -0,0 +1,27 @@
# ADR-053: Лента страницами по 50, без виртуализации списка
Уточняет [ADR-009](009-local-history.md): пагинация курсором остаётся, виртуализация снимается.
## Контекст
ADR-009 задаёт одной строкой две разные вещи: «пагинация курсором по ~50 сообщений, виртуализация списка в DOM». Первая — про данные и решает настоящую задачу: чат в десять тысяч сообщений не должен читаться из IndexedDB целиком при открытии. Вторая — про разметку, и её цена выяснилась только на этапе 5.
Виртуализация требует знать высоту строки до отрисовки. В ленте её нет: текст переносится, на десктопе строка — две ячейки грида через `display: contents` (`docs/identity/brief.md`), сообщение бывает в одну строку и в тридцать. Значит нужны измерение каждой строки, распорки сверху и снизу и пересчёт при смене ширины окна. Платят за это не только кодом: `aria-live` на ленте (`docs/ui.md`, «Доступность») зачитывает появление и исчезновение строк, а поиск по странице и выделение текста перестают видеть то, что убрано из разметки.
Выгоды при этом нет. В DOM попадает не вся история, а только то, что человек домотал прокруткой: открытие чата — 50 строк независимо от размера переписки.
## Решение
- Виртуализации в v1 нет. В разметке живёт всё загруженное.
- Лента открывается последней страницей в 50 сообщений и стоит в конце.
- Прокрутка к верхнему краю берёт следующие 50 назад по индексу `chat` (`docs/storage.md`). Страница короче полной означает, что выше ничего нет.
- Расстояние до низа при подгрузке сохраняется: то, что человек читает, не двигается.
- Страница короче окна прокрутки события `scroll` не порождает, поэтому следующая берётся сразу — пока лента не заполнит окно или сообщения не кончатся.
- Разделители дат и «новые» считаются по всему загруженному, а не по последней странице: граница «новых» уезжает вверх вместе с подгруженным.
- Записано в `docs/ui.md` («Чат») и `docs/architecture.md`.
## Следствия
- Домотавший до начала переписки в десять тысяч сообщений держит их все в разметке. Это его прокрутка и его выбор; обычное открытие чата — 50 строк.
- Экранный диктор, поиск по странице и выделение работают как в обычном документе.
- Возврат виртуализации — отдельный ADR, если появится жалоба, а не предположение.