Развести номера ADR: 064/065/066 этой ветки → 073/074/075

Ветка отведена от 522ba89, где последним был ADR-063, и заняла
064–072. Тем временем в main через #3 и #4 пришли свои 064
(строка ввода — div вместо form), 065 (стороны сообщений)
и 066 (серверного перца нет).

Переезжают три ADR этой ветки, 067–072 остаются на месте:
их номера свободны, а внутренние ссылки менять незачем.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR8uS3zGkybzWRW4GEY1oz
This commit is contained in:
2026-08-24 11:23:12 +03:00
co-authored by Claude Opus 5
parent 383e374f0f
commit f9ac83bef8
25 changed files with 41 additions and 41 deletions
+1 -1
View File
@@ -162,7 +162,7 @@ func vapid() error {
// version печатает, какой код собран в этот бинарь: короткую ревизию // version печатает, какой код собран в этот бинарь: короткую ревизию
// и время коммита. Время коммита, а не компиляции: штамп момента сборки // и время коммита. Время коммита, а не компиляции: штамп момента сборки
// делал бы каждую пересборку одного коммита новым файлом, и сверка хеша // делал бы каждую пересборку одного коммита новым файлом, и сверка хеша
// со сборкой из тега перестала бы что-либо значить (ADR-022, ADR-065). // со сборкой из тега перестала бы что-либо значить (ADR-022, ADR-074).
// Ревизию, которой нет, и время, которого нет, бинарь не выдумывает // Ревизию, которой нет, и время, которого нет, бинарь не выдумывает
// (ADR-057). // (ADR-057).
func version() { func version() {
+1 -1
View File
@@ -1,6 +1,6 @@
# ADR-022: Деплой — nginx, systemd, кросс-сборка # ADR-022: Деплой — nginx, systemd, кросс-сборка
Уточнён [ADR-032](032-state-permissions.md) (`StateDirectoryMode` и `UMask` в юните), [ADR-056](056-nginx-access-log-off.md) (`access_log off`), [ADR-057](057-version-marks-dirty-tree.md) (`bare version` помечает сборку из изменённого дерева) и [ADR-065](065-version-and-commit-time.md) (версия и время коммита в `GET /api/config`, времени компиляции в бинаре нет). Уточнён [ADR-032](032-state-permissions.md) (`StateDirectoryMode` и `UMask` в юните), [ADR-056](056-nginx-access-log-off.md) (`access_log off`), [ADR-057](057-version-marks-dirty-tree.md) (`bare version` помечает сборку из изменённого дерева) и [ADR-074](074-version-and-commit-time.md) (версия и время коммита в `GET /api/config`, времени компиляции в бинаре нет).
## Контекст ## Контекст
+1 -1
View File
@@ -1,6 +1,6 @@
# ADR-024: Айдентика «Скобы», интерфейс и язык # ADR-024: Айдентика «Скобы», интерфейс и язык
Уточнён [ADR-066](066-no-zoom-app-feel.md) (масштабирование запрещено, размеры шрифтов прежние), [ADR-067](067-version-in-sidebar-foot.md) (версия и время коммита в подвале сайдбара) и [ADR-069](069-message-input-is-editable-block.md) (строка сообщения — редактируемый блок). Уточнён [ADR-075](075-no-zoom-app-feel.md) (масштабирование запрещено, размеры шрифтов прежние), [ADR-067](067-version-in-sidebar-foot.md) (версия и время коммита в подвале сайдбара) и [ADR-069](069-message-input-is-editable-block.md) (строка сообщения — редактируемый блок).
## Контекст ## Контекст
+1 -1
View File
@@ -1,6 +1,6 @@
# ADR-047: Исходящий запрос к push-сервису # ADR-047: Исходящий запрос к push-сервису
Уточняет [ADR-011](011-web-push.md) и [ADR-023](023-push-and-service-worker.md). Код причины из ответа push-сервиса в журнале — [ADR-064](064-push-failure-reason-in-log.md). Уточняет [ADR-011](011-web-push.md) и [ADR-023](023-push-and-service-worker.md). Код причины из ответа push-сервиса в журнале — [ADR-073](073-push-failure-reason-in-log.md).
## Контекст ## Контекст
@@ -2,7 +2,7 @@
Уточняет [ADR-022](022-deploy-nginx-systemd.md): проверка подлинности бинаря опирается на ревизию, значит ревизия обязана быть честной. Уточняет [ADR-022](022-deploy-nginx-systemd.md): проверка подлинности бинаря опирается на ревизию, значит ревизия обязана быть честной.
Уточнён [ADR-065](065-version-and-commit-time.md): чтение build info переехало в `internal/build`, ревизия печатается короткой и рядом с временем коммита; `+dirty` и `unknown` — как здесь. Формат стал проверяемым тестом — разбор build info отделён от `debug.ReadBuildInfo`; сама простановка `vcs.*` по-прежнему проверяется руками. Уточнён [ADR-074](074-version-and-commit-time.md): чтение build info переехало в `internal/build`, ревизия печатается короткой и рядом с временем коммита; `+dirty` и `unknown` — как здесь. Формат стал проверяемым тестом — разбор build info отделён от `debug.ReadBuildInfo`; сама простановка `vcs.*` по-прежнему проверяется руками.
## Контекст ## Контекст
@@ -1,10 +1,10 @@
# ADR-067: Версия и время коммита в подвале сайдбара # ADR-067: Версия и время коммита в подвале сайдбара
Доводит до интерфейса [ADR-065](065-version-and-commit-time.md): сервер отдаёт `version` и `commitAt`, а где их показывать, там не решено. Доводит до интерфейса [ADR-074](074-version-and-commit-time.md): сервер отдаёт `version` и `commitAt`, а где их показывать, там не решено.
## Контекст ## Контекст
`GET /api/config` называет работающую версию и время коммита (ADR-065). Клиент читает конфигурацию при запуске и так — ради `kdfIterations` и `vapidPublicKey`. Показать их негде: `docs/ui.md` такого элемента не знает. `GET /api/config` называет работающую версию и время коммита (ADR-074). Клиент читает конфигурацию при запуске и так — ради `kdfIterations` и `vapidPublicKey`. Показать их негде: `docs/ui.md` такого элемента не знает.
Спрашивают об этом при каждой поломке: обновилось ли то, что человек видит. В установленном на «Домой» приложении вопрос острее — там нет ни адресной строки, ни консоли, и другого способа узнать версию у человека нет. Спрашивают об этом при каждой поломке: обновилось ли то, что человек видит. В установленном на «Домой» приложении вопрос острее — там нет ни адресной строки, ни консоли, и другого способа узнать версию у человека нет.
@@ -13,7 +13,7 @@
- Версия стоит в подвале сайдбара, рядом с «ты: @nick»: это единственное постоянное место интерфейса, где уже написано, кто и где мы. - Версия стоит в подвале сайдбара, рядом с «ты: @nick»: это единственное постоянное место интерфейса, где уже написано, кто и где мы.
- Текст — `версия · дд.мм чч:мм`, время коммита в местной зоне. Год не показывается: вопрос «что сейчас работает», а не летопись. Времени нет (`commitAt = 0`) — остаётся одна версия; конфигурации нет вовсе — строки нет. - Текст — `версия · дд.мм чч:мм`, время коммита в местной зоне. Год не показывается: вопрос «что сейчас работает», а не летопись. Времени нет (`commitAt = 0`) — остаётся одна версия; конфигурации нет вовсе — строки нет.
- Цвет `stone`, размер подписи 11 px, без рамок и фона. Акцентом версия не бывает: `mark` — один смысловой элемент на экран (ADR-024), и это непрочитанные, а не служебная строка. - Цвет `stone`, размер подписи 11 px, без рамок и фона. Акцентом версия не бывает: `mark` — один смысловой элемент на экран (ADR-024), и это непрочитанные, а не служебная строка.
- Не кнопка и не ссылка: нажимать в ней нечего. Стоит рядом с кнопкой «ты: @nick», а не внутри неё — иначе экранный диктор зачитывал бы хеш как часть названия кнопки. Выделяется, в отличие от остального сайдбара (ADR-066): на iPhone скопировать её иначе нечем. - Не кнопка и не ссылка: нажимать в ней нечего. Стоит рядом с кнопкой «ты: @nick», а не внутри неё — иначе экранный диктор зачитывал бы хеш как часть названия кнопки. Выделяется, в отличие от остального сайдбара (ADR-075): на iPhone скопировать её иначе нечем.
- В сайдбаре 224 px строка и версия рядом не помещаются никогда: `версия · дд.мм чч:мм` — это 179 px при внутренней ширине 184, а «ты: @nick» просит ещё восемьдесят с лишним. Значит, на десктопе версия всегда занимает вторую строку и стоит справа; на мобильном, где сайдбар во всю ширину, обе стоят в одной строке. Ник, которому не хватило и целой строки, обрезается многоточием; версия — никогда: половина хеша бесполезна. - В сайдбаре 224 px строка и версия рядом не помещаются никогда: `версия · дд.мм чч:мм` — это 179 px при внутренней ширине 184, а «ты: @nick» просит ещё восемьдесят с лишним. Значит, на десктопе версия всегда занимает вторую строку и стоит справа; на мобильном, где сайдбар во всю ширину, обе стоят в одной строке. Ник, которому не хватило и целой строки, обрезается многоточием; версия — никогда: половина хеша бесполезна.
- Текст ради одной строки не режется: вариант «в подвале ревизия, время в `title`» отклонён — `title` не показывается на телефоне, а телефон и есть место, ради которого строка заведена. - Текст ради одной строки не режется: вариант «в подвале ревизия, время в `title`» отклонён — `title` не показывается на телефоне, а телефон и есть место, ради которого строка заведена.
@@ -23,4 +23,4 @@
- Разговор о поломке начинается с версии, а не с «попробуйте обновиться». - Разговор о поломке начинается с версии, а не с «попробуйте обновиться».
- Подвал сайдбара стал двухстрочным и вырос с 47 px мока до 76. Это принятое отклонение от `docs/identity/screens.html`, блок 2a: там подвал — одна строка «ты: @nick» без версии и без 44 px цели нажатия. Мок остаётся снимком айдентики на момент ADR-024 и за интерфейсом не идёт: в нём нет и строки `+ новый чат`, и баннера установки. - Подвал сайдбара стал двухстрочным и вырос с 47 px мока до 76. Это принятое отклонение от `docs/identity/screens.html`, блок 2a: там подвал — одна строка «ты: @nick» без версии и без 44 px цели нажатия. Мок остаётся снимком айдентики на момент ADR-024 и за интерфейсом не идёт: в нём нет и строки `+ новый чат`, и баннера установки.
- Названа версия сервера, а не оболочки, которую исполняет браузер: `version` приезжает с `GET /api/config`, а страница и её модули — из кэша service worker. После релиза первое открытие показывает новую версию под старой оболочкой; расходятся они до применения обновления (ADR-068), а с недописанным сообщением в поле — сколь угодно долго. Показывать рядом версию кэша — отдельное решение, здесь его нет. - Названа версия сервера, а не оболочки, которую исполняет браузер: `version` приезжает с `GET /api/config`, а страница и её модули — из кэша service worker. После релиза первое открытие показывает новую версию под старой оболочкой; расходятся они до применения обновления (ADR-068), а с недописанным сообщением в поле — сколь угодно долго. Показывать рядом версию кэша — отдельное решение, здесь его нет.
- Записано в `docs/ui.md`, «Список чатов (сайдбар)». Отдельного запроса ради этого нет: поля приезжают с конфигурацией (ADR-065). - Записано в `docs/ui.md`, «Список чатов (сайдбар)». Отдельного запроса ради этого нет: поля приезжают с конфигурацией (ADR-074).
@@ -1,12 +1,12 @@
# ADR-069: Строка сообщения — редактируемый блок, а не поле формы # ADR-069: Строка сообщения — редактируемый блок, а не поле формы
Уточняет [ADR-024](024-identity-and-ui.md) («Чат» и «Доступность» в `docs/ui.md`) и [ADR-066](066-no-zoom-app-feel.md): в перечне элементов управления, по которым второй тап не гасится, прибавился редактируемый блок. Уточняет [ADR-024](024-identity-and-ui.md) («Чат» и «Доступность» в `docs/ui.md`) и [ADR-075](075-no-zoom-app-feel.md): в перечне элементов управления, по которым второй тап не гасится, прибавился редактируемый блок.
Уточнён [ADR-071](071-input-limit-cuts-what-arrives.md): предел держится до вставки и режет приходящее, а не хвост блока. Уточнён [ADR-071](071-input-limit-cuts-what-arrives.md): предел держится до вставки и режет приходящее, а не хвост блока.
## Контекст ## Контекст
На iPhone при фокусе в строке сообщения над клавиатурой висит системная полоса помощника форм: две стрелки перехода между полями и «готово». Стрелки неактивны — поле на экране одно, переходить некуда, — а полоса занимает место и ломает ощущение приложения, ради которого принят ADR-066. На iPhone при фокусе в строке сообщения над клавиатурой висит системная полоса помощника форм: две стрелки перехода между полями и «готово». Стрелки неактивны — поле на экране одно, переходить некуда, — а полоса занимает место и ломает ощущение приложения, ради которого принят ADR-075.
Убрать её со страницы нечем: полосу рисует система, и для `input` и `textarea` она бывает всегда. Единственный работающий приём — перестать быть полем формы: у редактируемого блока (`contenteditable`) iOS полосы не показывает. Убрать её со страницы нечем: полосу рисует система, и для `input` и `textarea` она бывает всегда. Единственный работающий приём — перестать быть полем формы: у редактируемого блока (`contenteditable`) iOS полосы не показывает.
@@ -1,4 +1,4 @@
# ADR-064: Код причины отказа push-сервиса в журнале # ADR-073: Код причины отказа push-сервиса в журнале
Уточняет [ADR-047](047-push-endpoint.md). Уточняет [ADR-047](047-push-endpoint.md).
@@ -1,4 +1,4 @@
# ADR-065: Версия — короткая ревизия, «время сборки» — время коммита # ADR-074: Версия — короткая ревизия, «время сборки» — время коммита
Уточняет [ADR-022](022-deploy-nginx-systemd.md) (подлинность бинаря проверяется сверкой хеша со сборкой из тега) и [ADR-057](057-version-marks-dirty-tree.md) (`+dirty` у сборки из изменённого дерева). Уточняет [ADR-022](022-deploy-nginx-systemd.md) (подлинность бинаря проверяется сверкой хеша со сборкой из тега) и [ADR-057](057-version-marks-dirty-tree.md) (`+dirty` у сборки из изменённого дерева).
@@ -1,4 +1,4 @@
# ADR-066: Масштабирования нет, размеры шрифтов прежние # ADR-075: Масштабирования нет, размеры шрифтов прежние
Уточняет [ADR-024](024-identity-and-ui.md): типографика из `docs/identity/brief.md` остаётся как записана, а страница перестаёт масштабироваться. Уточняет [ADR-024](024-identity-and-ui.md): типографика из `docs/identity/brief.md` остаётся как записана, а страница перестаёт масштабироваться.
+2 -2
View File
@@ -8,7 +8,7 @@
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bare ./cmd/bare GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bare ./cmd/bare
``` ```
Версия бинаря — `vcs.revision` и `vcs.time` из `debug.ReadBuildInfo()`. `bare version` печатает семь символов ревизии и время коммита в UTC: `cfd0ec0 2026-08-23T04:54:16Z`. У сборки из изменённого рабочего дерева (`vcs.modified`) к ревизии дописывается `+dirty` — сверка со сборкой из тега не должна проходить молча (ADR-057); у сборки не из git печатается `unknown`. Времени компиляции в бинаре нет: оно делало бы каждую пересборку одного коммита новым файлом (ADR-065). Те же значения отдаёт `GET /api/config` полями `version` и `commitAt`. `/healthz` отвечает только `ok`. Версия бинаря — `vcs.revision` и `vcs.time` из `debug.ReadBuildInfo()`. `bare version` печатает семь символов ревизии и время коммита в UTC: `cfd0ec0 2026-08-23T04:54:16Z`. У сборки из изменённого рабочего дерева (`vcs.modified`) к ревизии дописывается `+dirty` — сверка со сборкой из тега не должна проходить молча (ADR-057); у сборки не из git печатается `unknown`. Времени компиляции в бинаре нет: оно делало бы каждую пересборку одного коммита новым файлом (ADR-074). Те же значения отдаёт `GET /api/config` полями `version` и `commitAt`. `/healthz` отвечает только `ok`.
## Первичная настройка сервера (один раз) ## Первичная настройка сервера (один раз)
@@ -147,6 +147,6 @@ ssh xmatic 'sudo install -m 0755 -o root -g root /tmp/bare /opt/bare/bare && sud
## Логи ## Логи
Сервер пишет в stdout: время, метод, путь, статус, длительность; для маршрутов `/api/` вместо пути пишется шаблон (`/api/users/{nick}`), чтобы ник не попадал в журнал, а если отказ случился до маршрутизации (`Origin`, предел тела) и шаблона ещё нет — просто `/api/`; ника в журнале нет вовсе, включая отказы по лимитам (ADR-055); IP не пишется. Причины ответов `500 internal` (ADR-027) пишутся отдельной строкой, без данных запроса. Отправитель пушей пишет класс отказа — «таймаут», «имя не разрешилось», «отправка не удалась» — без адреса подписки и идентификатора устройства (ADR-047). Ответ push-сервиса пишется статусом и коротким кодом причины из тела: «пуш: push-сервис ответил 403 (BadJwtToken)». Код — диагностика вендора; всё, что на короткий код не похоже, отбрасывается целиком, и остаётся один статус (ADR-064). journald хранит по своим правилам. Сервер пишет в stdout: время, метод, путь, статус, длительность; для маршрутов `/api/` вместо пути пишется шаблон (`/api/users/{nick}`), чтобы ник не попадал в журнал, а если отказ случился до маршрутизации (`Origin`, предел тела) и шаблона ещё нет — просто `/api/`; ника в журнале нет вовсе, включая отказы по лимитам (ADR-055); IP не пишется. Причины ответов `500 internal` (ADR-027) пишутся отдельной строкой, без данных запроса. Отправитель пушей пишет класс отказа — «таймаут», «имя не разрешилось», «отправка не удалась» — без адреса подписки и идентификатора устройства (ADR-047). Ответ push-сервиса пишется статусом и коротким кодом причины из тела: «пуш: push-сервис ответил 403 (BadJwtToken)». Код — диагностика вендора; всё, что на короткий код не похоже, отбрасывается целиком, и остаётся один статус (ADR-073). journald хранит по своим правилам.
nginx журнал запросов не ведёт: `access_log off` в обоих server-блоках (ADR-056). Без этой строки он унаследовал бы `access.log` формата `combined` из `/etc/nginx/nginx.conf` — с адресом клиента и полным URI, то есть с ником и социальным графом. `error_log` остаётся: это журнал сбоев, а не запросов, и при отказе он записывает адрес клиента. nginx журнал запросов не ведёт: `access_log off` в обоих server-блоках (ADR-056). Без этой строки он унаследовал бы `access.log` формата `combined` из `/etc/nginx/nginx.conf` — с адресом клиента и полным URI, то есть с ником и социальным графом. `error_log` остаётся: это журнал сбоев, а не запросов, и при отказе он записывает адрес клиента.
+2 -2
View File
@@ -37,12 +37,12 @@ font-family: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monos
Размеры: текст сообщений 14 px / 1.55; автор и время 12 px; заголовки секций 10 px, разрядка 0.14em, uppercase; подписи 11 px; имя чата в шапке 15 px. Шрифты не загружаются. Размеры: текст сообщений 14 px / 1.55; автор и время 12 px; заголовки секций 10 px, разрядка 0.14em, uppercase; подписи 11 px; имя чата в шапке 15 px. Шрифты не загружаются.
Поля ввода — те же 14 px, исключений нет: масштабирование в клиенте запрещено, и растить шрифт ради обхода фокус-зума iOS не приходится (ADR-066). Поля ввода — те же 14 px, исключений нет: масштабирование в клиенте запрещено, и растить шрифт ради обхода фокус-зума iOS не приходится (ADR-075).
## Компоновка ## Компоновка
- Десктоп: сайдбар 224 px с правой границей `line`, шапка 64 px, отступы контента 32 px; сообщения — сетка `132px 1fr`, column-gap 20, row-gap 6. - Десктоп: сайдбар 224 px с правой границей `line`, шапка 64 px, отступы контента 32 px; сообщения — сетка `132px 1fr`, column-gap 20, row-gap 6.
- Мобильный: шапка 56 px, отступы 20 px, ввод с min-height 44 px. Нижнее поле подвала и строки ввода считается с `env(safe-area-inset-bottom)`: под системную полосу iPhone они не заходят (ADR-066). - Мобильный: шапка 56 px, отступы 20 px, ввод с min-height 44 px. Нижнее поле подвала и строки ввода считается с `env(safe-area-inset-bottom)`: под системную полосу iPhone они не заходят (ADR-075).
- Ввод — рамка 1 px ink, без скруглений, `>` цветом mark слева. - Ввод — рамка 1 px ink, без скруглений, `>` цветом mark слева.
- Активный элемент списка — инверсия: фон ink, текст bone. - Активный элемент списка — инверсия: фон ink, текст bone.
- Разделители — 1 px `line`; разделитель «новые» — 1 px mark. - Разделители — 1 px `line`; разделитель «новые» — 1 px mark.
+2 -2
View File
@@ -18,7 +18,7 @@
``` ```
embed.go //go:embed web в корне модуля (ADR-025) embed.go //go:embed web в корне модуля (ADR-025)
cmd/bare/main.go подкоманды: serve, vapid, version cmd/bare/main.go подкоманды: serve, vapid, version
internal/build/ ревизия и время коммита из build info (ADR-065) internal/build/ ревизия и время коммита из build info (ADR-074)
internal/config/ переменные BARE_* internal/config/ переменные BARE_*
internal/store/ SQLite, migrations/*.sql (embed), запросы internal/store/ SQLite, migrations/*.sql (embed), запросы
internal/auth/ argon2id, сессии, cookie internal/auth/ argon2id, сессии, cookie
@@ -36,7 +36,7 @@ web/
js/sync.js устройство, поток событий, приём и отправка js/sync.js устройство, поток событий, приём и отправка
js/pwa.js service worker, самообновление (ADR-068), пуши, установка js/pwa.js service worker, самообновление (ADR-068), пуши, установка
js/ulid.js ULID js/ulid.js ULID
js/zoom.js запрет масштабирования (ADR-066) js/zoom.js запрет масштабирования (ADR-075)
js/ui/*.js экраны из ui.md js/ui/*.js экраны из ui.md
js/export.js .bare js/export.js .bare
scripts/deploy.sh scripts/deploy.sh
+1 -1
View File
@@ -44,7 +44,7 @@ WrappedKey { to: nick, iv: string, ct: string }
`GET /api/config``200 {inviteRequired: bool, vapidPublicKey: string, kdfIterations: number, maxMessageChars: 4000, version: string, commitAt: number}` `GET /api/config``200 {inviteRequired: bool, vapidPublicKey: string, kdfIterations: number, maxMessageChars: 4000, version: string, commitAt: number}`
`version` — короткая ревизия сборки: семь символов хеша коммита, с суффиксом `+dirty` у бинаря из изменённого дерева (ADR-057) и `unknown` у сборки не из git. `commitAt` — время коммита в миллисекундах Unix, `0` если оно неизвестно. Это время коммита, а не момент компиляции: штамп времени сборки лишил бы смысла сверку хеша бинаря со сборкой из тега (ADR-022, ADR-065). Те же значения печатает `bare version`. Отдельного эндпоинта у них нет. `version` — короткая ревизия сборки: семь символов хеша коммита, с суффиксом `+dirty` у бинаря из изменённого дерева (ADR-057) и `unknown` у сборки не из git. `commitAt` — время коммита в миллисекундах Unix, `0` если оно неизвестно. Это время коммита, а не момент компиляции: штамп времени сборки лишил бы смысла сверку хеша бинаря со сборкой из тега (ADR-022, ADR-074). Те же значения печатает `bare version`. Отдельного эндпоинта у них нет.
`GET /api/kdf?nick=<nick>``200 {iterations}`. Для неизвестного ника — `kdfIterations` из конфигурации, тем же статусом. Скрытием существования ника ответ не занимается: у аккаунта, не входившего после повышения цели, число итераций своё (ADR-062), а сам факт, что ник существует, публичен (ADR-019). `GET /api/kdf?nick=<nick>``200 {iterations}`. Для неизвестного ника — `kdfIterations` из конфигурации, тем же статусом. Скрытием существования ника ответ не занимается: у аккаунта, не входившего после повышения цели, число итераций своё (ADR-062), а сам факт, что ник существует, публичен (ADR-019).
+2 -2
View File
@@ -8,7 +8,7 @@
Без inline-стилей и inline-скриптов (CSP). Рендер — `document.createElement` и `textContent`; `innerHTML` не используется нигде: сообщения — пользовательские данные. Без inline-стилей и inline-скриптов (CSP). Рендер — `document.createElement` и `textContent`; `innerHTML` не используется нигде: сообщения — пользовательские данные.
Страница ведёт себя как приложение, а не как документ (ADR-066): масштабирования нет, резинового отскока нет, выделение снято с шапки, сайдбара, кнопок и разделителей — но не с текста сообщений, отпечатков, полей ввода и версии в подвале сайдбара: их копируют. Установленное на «Домой» приложение объявлено мета-строками, имя на экране — `bare`, системная полоса остаётся над страницей. Подвал сайдбара и строка ввода отступают от нижней системной полосы iPhone. Страница ведёт себя как приложение, а не как документ (ADR-075): масштабирования нет, резинового отскока нет, выделение снято с шапки, сайдбара, кнопок и разделителей — но не с текста сообщений, отпечатков, полей ввода и версии в подвале сайдбара: их копируют. Установленное на «Домой» приложение объявлено мета-строками, имя на экране — `bare`, системная полоса остаётся над страницей. Подвал сайдбара и строка ввода отступают от нижней системной полосы iPhone.
## Вход и регистрация ## Вход и регистрация
@@ -120,4 +120,4 @@
Строка сообщения — редактируемый блок (ADR-069), и семантику ему задают руками: `role="textbox"`, `aria-multiline="true"`, подпись `aria-label` тем же текстом, что подсказка. Заблокированный ввод перестаёт быть редактируемым и говорит об этом `aria-disabled`. Строка сообщения — редактируемый блок (ADR-069), и семантику ему задают руками: `role="textbox"`, `aria-multiline="true"`, подпись `aria-label` тем же текстом, что подсказка. Заблокированный ввод перестаёт быть редактируемым и говорит об этом `aria-disabled`.
Масштабирование запрещено — и щипок, и двойной тап (ADR-066). Это сознательный размен: интерфейс ведёт себя как приложение, а человек, которому нужно увеличить мелкое, средства лишается. Размеры шрифтов от запрета не меняются: поля ввода остаются 14 px, как в `docs/identity/brief.md`. Браузер вправе запрет проигнорировать. Масштабирование запрещено — и щипок, и двойной тап (ADR-075). Это сознательный размен: интерфейс ведёт себя как приложение, а человек, которому нужно увеличить мелкое, средства лишается. Размеры шрифтов от запрета не меняются: поля ввода остаются 14 px, как в `docs/identity/brief.md`. Браузер вправе запрет проигнорировать.
+1 -1
View File
@@ -17,7 +17,7 @@ import (
// //
// version и commitAt отвечают на вопрос «какой код сейчас работает»: // version и commitAt отвечают на вопрос «какой код сейчас работает»:
// короткая ревизия сборки и время коммита, а не момент компиляции // короткая ревизия сборки и время коммита, а не момент компиляции
// (ADR-065). Отдельного эндпоинта им не заводится — конфигурацию клиент // (ADR-074). Отдельного эндпоинта им не заводится — конфигурацию клиент
// читает до входа и так. // читает до входа и так.
func (s *server) config(w http.ResponseWriter, r *http.Request) { func (s *server) config(w http.ResponseWriter, r *http.Request) {
info := build.Current() info := build.Current()
+1 -1
View File
@@ -112,7 +112,7 @@ func TestConfig(t *testing.T) {
t.Errorf("maxMessageChars: получено %d, ожидалось 4000", got.MaxMessageChars) t.Errorf("maxMessageChars: получено %d, ожидалось 4000", got.MaxMessageChars)
} }
// Версия — то же самое, что печатает `bare version`: одно место, // Версия — то же самое, что печатает `bare version`: одно место,
// один формат (ADR-065). Тестовому бинарю vcs.* не проставляются, // один формат (ADR-074). Тестовому бинарю vcs.* не проставляются,
// поэтому здесь проверяется в том числе поведение без build info — // поэтому здесь проверяется в том числе поведение без build info —
// «unknown» и 0. // «unknown» и 0.
if !validVersion(got.Version) { if !validVersion(got.Version) {
+1 -1
View File
@@ -1,5 +1,5 @@
// Package build отвечает на вопрос «какой код сейчас работает»: ревизия // Package build отвечает на вопрос «какой код сейчас работает»: ревизия
// и время коммита, которые git оставляет в бинаре при сборке (ADR-065). // и время коммита, которые git оставляет в бинаре при сборке (ADR-074).
// Момента компиляции здесь нет и не будет: штамп времени сборки делал бы // Момента компиляции здесь нет и не будет: штамп времени сборки делал бы
// каждую пересборку одного коммита новым файлом, а подлинность бинаря // каждую пересборку одного коммита новым файлом, а подлинность бинаря
// проверяется сравнением хеша со сборкой из тега (ADR-022). // проверяется сравнением хеша со сборкой из тега (ADR-022).
+1 -1
View File
@@ -9,7 +9,7 @@ import (
const hash = "9f2c1ab7d3e4c5061728394a5b6c7d8e9f001122" const hash = "9f2c1ab7d3e4c5061728394a5b6c7d8e9f001122"
// Формат версии: семь символов ревизии, «+dirty» у изменённого дерева, // Формат версии: семь символов ревизии, «+dirty» у изменённого дерева,
// «unknown» без ревизии (ADR-057, ADR-065). // «unknown» без ревизии (ADR-057, ADR-074).
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {
cases := []struct { cases := []struct {
info Info info Info
+4 -4
View File
@@ -70,7 +70,7 @@ const (
dropEvery = time.Minute dropEvery = time.Minute
) )
// Чтение тела ответа push-сервиса ради кода причины (ADR-064). // Чтение тела ответа push-сервиса ради кода причины (ADR-073).
const ( const (
// maxReasonBody — сколько байт тела читаем. Код причины стоит в начале // maxReasonBody — сколько байт тела читаем. Код причины стоит в начале
// ответа; остальное дочитывается в никуда, ради переиспользования // ответа; остальное дочитывается в никуда, ради переиспользования
@@ -331,7 +331,7 @@ func (s *Sender) deliver(j job) {
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
// Начало тела нужно ради кода причины (ADR-064), остаток дочитывается // Начало тела нужно ради кода причины (ADR-073), остаток дочитывается
// в никуда: иначе соединение не переиспользуется. // в никуда: иначе соединение не переиспользуется.
head, _ := io.ReadAll(io.LimitReader(resp.Body, maxReasonBody)) head, _ := io.ReadAll(io.LimitReader(resp.Body, maxReasonBody))
io.Copy(io.Discard, resp.Body) io.Copy(io.Discard, resp.Body)
@@ -432,7 +432,7 @@ func (s *Sender) report(format string, args ...any) {
} }
// status — ответ push-сервиса для журнала: код и, если он разобран, // status — ответ push-сервиса для журнала: код и, если он разобран,
// короткий код причины из тела (ADR-064). // короткий код причины из тела (ADR-073).
func status(code int, body []byte) string { func status(code int, body []byte) string {
if r := serviceReason(body); r != "" { if r := serviceReason(body); r != "" {
return fmt.Sprintf("%d (%s)", code, r) return fmt.Sprintf("%d (%s)", code, r)
@@ -443,7 +443,7 @@ func status(code int, body []byte) string {
// serviceReason достаёт из тела ответа короткий код причины: APNs отвечает // serviceReason достаёт из тела ответа короткий код причины: APNs отвечает
// {"reason":"BadJwtToken"}, Mozilla — {"errno":…,"error":"Not Found"}. // {"reason":"BadJwtToken"}, Mozilla — {"errno":…,"error":"Not Found"}.
// Код — диагностика вендора, а не данные пользователя, и без него отказ // Код — диагностика вендора, а не данные пользователя, и без него отказ
// не читается: голый «403» сутки выглядел как «что-то с пушами» (ADR-064). // не читается: голый «403» сутки выглядел как «что-то с пушами» (ADR-073).
// //
// Тело всё же приходит снаружи, поэтому в журнал идёт не оно, а то, что // Тело всё же приходит снаружи, поэтому в журнал идёт не оно, а то, что
// прошло safeReason. // прошло safeReason.
+2 -2
View File
@@ -193,7 +193,7 @@ func TestVAPIDSubjectInToken(t *testing.T) {
} }
// Отказ push-сервиса читается по журналу: статус и код причины из тела // Отказ push-сервиса читается по журналу: статус и код причины из тела
// (ADR-064). Без кода 403 от APNs неотличим от любого другого отказа. // (ADR-073). Без кода 403 от APNs неотличим от любого другого отказа.
func TestServiceStatusInLog(t *testing.T) { func TestServiceStatusInLog(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(io.Discard, r.Body) io.Copy(io.Discard, r.Body)
@@ -225,7 +225,7 @@ func TestServiceStatusInLog(t *testing.T) {
// Код причины берётся из тела ответа, но телом распоряжается чужая // Код причины берётся из тела ответа, но телом распоряжается чужая
// сторона: всё, что на короткий код не похоже, в журнал не идёт вовсе // сторона: всё, что на короткий код не похоже, в журнал не идёт вовсе
// (ADR-064, docs/deploy.md, «Логи»). // (ADR-073, docs/deploy.md, «Логи»).
func TestServiceReason(t *testing.T) { func TestServiceReason(t *testing.T) {
const endpoint = "web.push.apple.com" const endpoint = "web.push.apple.com"
cases := []struct{ body, want string }{ cases := []struct{ body, want string }{
+5 -5
View File
@@ -25,7 +25,7 @@ body {
height: 100%; height: 100%;
} }
/* масштабирования нет (ADR-066). manipulation снимает зум по двойному тапу /* масштабирования нет (ADR-075). manipulation снимает зум по двойному тапу
и задержку 300 мс, которую браузер держит, ожидая второго касания; и задержку 300 мс, которую браузер держит, ожидая второго касания;
прокрутку и щипок правило не трогает — щипок запрещён в viewport. прокрутку и щипок правило не трогает — щипок запрещён в viewport.
Правило стоит на html: двойной тап — жест документа, и браузер берёт Правило стоит на html: двойной тап — жест документа, и браузер берёт
@@ -41,7 +41,7 @@ body {
/* виден вырез: viewport-fit=cover пускает страницу под скруглённые углы, /* виден вырез: viewport-fit=cover пускает страницу под скруглённые углы,
под боковой вырез в альбомной ориентации и под верхнюю системную под боковой вырез в альбомной ориентации и под верхнюю системную
полосу. Полосу над страницей держит мета-строка о стиле полосы полосу. Полосу над страницей держит мета-строка о стиле полосы
(ADR-066), поэтому верхняя вставка обычно нулевая и не делает ничего; (ADR-075), поэтому верхняя вставка обычно нулевая и не делает ничего;
там, где она не нулевая, шапка не уезжает под часы */ там, где она не нулевая, шапка не уезжает под часы */
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left); padding-left: env(safe-area-inset-left);
@@ -58,7 +58,7 @@ body {
/* интерфейс не выделяется: долгое нажатие по шапке, списку и кнопкам /* интерфейс не выделяется: долгое нажатие по шапке, списку и кнопкам
вызывает лупу и «копировать», а копировать там нечего. Текст сообщений, вызывает лупу и «копировать», а копировать там нечего. Текст сообщений,
отпечатки и поля ввода выделяются как обычно — их именно копируют отпечатки и поля ввода выделяются как обычно — их именно копируют
(ADR-066) */ (ADR-075) */
.side, .side,
.head, .head,
@@ -421,7 +421,7 @@ input[type="password"] {
gap: 2px 8px; gap: 2px 8px;
margin-top: auto; margin-top: auto;
/* нижняя системная полоса iPhone проходит поверх страницы: подвал /* нижняя системная полоса iPhone проходит поверх страницы: подвал
отступает от неё, а не прячется под ней (ADR-066) */ отступает от неё, а не прячется под ней (ADR-075) */
padding: 6px 20px calc(6px + env(safe-area-inset-bottom)); padding: 6px 20px calc(6px + env(safe-area-inset-bottom));
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
@@ -463,7 +463,7 @@ input[type="password"] {
/* версия выделяется: с неё начинается разговор о поломке (ADR-067), /* версия выделяется: с неё начинается разговор о поломке (ADR-067),
а на iPhone скопировать её иначе нечем — ни строки адреса, ни консоли а на iPhone скопировать её иначе нечем — ни строки адреса, ни консоли
там нет. Общий запрет выделения снят с неё так же, как с текста там нет. Общий запрет выделения снят с неё так же, как с текста
сообщений и отпечатков (ADR-066) */ сообщений и отпечатков (ADR-075) */
.build { .build {
flex: none; flex: none;
+1 -1
View File
@@ -483,7 +483,7 @@ function errorText(err) {
async function boot() { async function boot() {
// Масштабирования нет: интерфейс ведёт себя как приложение, а не как // Масштабирования нет: интерфейс ведёт себя как приложение, а не как
// страница (ADR-066). Строке viewport Safari верит не всегда, поэтому // страница (ADR-075). Строке viewport Safari верит не всегда, поэтому
// страховка ставится до первого экрана. // страховка ставится до первого экрана.
lock(); lock();
db.persist(); db.persist();
+1 -1
View File
@@ -61,7 +61,7 @@ function side(ctx, active) {
return { nav, dispose: mount(items, ctx, active) }; return { nav, dispose: mount(items, ctx, active) };
} }
// build — версия и время коммита из GET /api/config (ADR-065, ADR-067). // build — версия и время коммита из GET /api/config (ADR-074, ADR-067).
// Конфигурации нет — офлайн-старт до первого ответа сервера — значит, // Конфигурации нет — офлайн-старт до первого ответа сервера — значит,
// и строки нет: выдумывать версию не из чего. Время без версии не бывает: // и строки нет: выдумывать версию не из чего. Время без версии не бывает:
// её сервер отдаёт всегда, хотя бы как «unknown». // её сервер отдаёт всегда, хотя бы как «unknown».
+1 -1
View File
@@ -1,4 +1,4 @@
// Запрет масштабирования — страховка к строке viewport (ADR-066). // Запрет масштабирования — страховка к строке viewport (ADR-075).
// //
// Основное средство — `maximum-scale=1, user-scalable=no` в index.html // Основное средство — `maximum-scale=1, user-scalable=no` в index.html
// и `touch-action: manipulation` в app.css. Safari вправе не послушаться: // и `touch-action: manipulation` в app.css. Safari вправе не послушаться: