diff --git a/docs/architecture.md b/docs/architecture.md index 5bf9a73..2d84bc0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -120,8 +120,17 @@ другим. Данные не переносим — начинаем с чистого листа. - **Учётные записи.** Вход через OIDC, провайдер — Authelia. Не решено, где живёт сессия и как связываются пользователь Telegram и пользователь веба. -- **Веб-интерфейс.** Формы нет вовсе, есть только API. Конвенция веб-UI на htmx - описана в [conventions/web-ui.md](conventions/web-ui.md) заранее. +- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA, + устанавливаемое на телефон; фреймворк выбирает разведка + `spa-framework-choice`, и до её итога + [conventions/web-ui.md](conventions/web-ui.md) стоит почти пустой. Шаг сборки + фронтенда меняет требования к машине разработчика и к образу — решение уровня + ADR. +- **Уведомления.** Пользователь веба узнаёт о готовности только опросом. + Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web + Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и + текст расшифровки начинает уходить на сторону — сдвиг периметра + [security.md](security.md). - **Долгие записи.** Потолок сегодня неизвестен и не замерялся: 20 МиБ на приём из Telegram — точно, ограничения `deferred-general` по длине — нет. - **Формат для распознавания.** Конвертер отдаёт ogg/vorbis (`libvorbis`), а diff --git a/docs/conventions/README.md b/docs/conventions/README.md index 58e7462..2804e98 100644 --- a/docs/conventions/README.md +++ b/docs/conventions/README.md @@ -15,11 +15,14 @@ severity — в [CLAUDE.md](../../CLAUDE.md). ## Откуда взяты и что с расхождениями -Все пять записей перенесены из проекта jellybit — тот же Go, тот же автор, те же +Четыре записи перенесены из проекта jellybit — тот же Go, тот же автор, те же задачи. Код transcriber написан раньше и **части правил не следует**: ключи — UUID вместо ULID, время берётся `time.Now()` по месту, лог пишется на каждом шаге и дублируется воркером, доменные ошибки проверяются приведением типа. +Пятая, `web-ui.md`, тоже пришла оттуда, но не прижилась: jellybit работает на +htmx, а здесь решено делать SPA — и перенесённый текст снят целиком. + Каждое такое место названо в своей записи строкой «*Расхождение:*». Читается оно как **долг, а не как нарушение**: правила действуют на новый код, переписывание существующего — отдельная работа. Проходу ревью строка «Расхождение» говорит, что @@ -36,10 +39,10 @@ UUID вместо ULID, время берётся `time.Now()` по месту, `0600`, самодокументируемый `config.dist.toml`, проверка на старте. - [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT ULID, разбор на входной границе, естественные ключи у деталей. -- [web-ui.md](web-ui.md) — веб-UI на htmx: партиал равен странице равен - фрагменту, ветвление по `HX-Request`, деградация без JS, ошибка на htmx-пути - как 200 плюс фрагмент, самозавершающийся опрос, вендоринг статики. **Записана - наперёд: веб-UI ещё нет.** +- [web-ui.md](web-ui.md) — веб-UI: что решено про приложение (SPA, установка на + телефон, статика в бинарнике) и что ждёт выбора фреймворка. **Почти пуста:** + редакция на htmx снята 2026-08-10 вместе со сменой решения, а новую писать не + под что до разведки `spa-framework-choice`. ## Механизировано diff --git a/docs/conventions/web-ui.md b/docs/conventions/web-ui.md index af959bc..5505fee 100644 --- a/docs/conventions/web-ui.md +++ b/docs/conventions/web-ui.md @@ -1,146 +1,41 @@ -# Веб-UI (htmx) +# Веб-UI -Конвенция: *как* мы пишем код веб-UI — частичная замена фрагментов, опрос живых -обновлений, обработчики действий, деградация без JS, ошибки. Это правила -оформления кода (How), а не спецификация поведения — что именно UI показывает и -какие действия обязан поддерживать, живёт в спеке OpenSpec. +Конвенция: *как* мы пишем код приложения. Это правила оформления кода (How), а не +спецификация поведения — что именно приложение показывает и какие действия +обязано поддерживать, живёт в спеке OpenSpec. -**Взято из проекта jellybit и записано наперёд: веб-UI в transcriber нет вовсе.** -Есть только HTTP API на gin. Ни одного расхождения назвать нельзя — нечему -расходиться; правила действуют с первой страницы, которую заведём. +**Фреймворк не выбран, и до выбора эта конвенция пуста.** Здесь стоит только то, +что решено и от фреймворка не зависит; всё остальное появится по итогу разведки +`spa-framework-choice`, которая же заведёт запись в `research/` и ADR. + +Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные +фреймворки. Решение сменилось на SPA 2026-08-10, и та редакция снята целиком: +частичная подмена фрагментов, ветвление по `HX-Request` и деградация без JS к +SPA не относятся ни одним пунктом. Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок -наружу — [errors.md](errors.md). Здесь — только особенности htmx-транспорта, без -повторения. +наружу — [errors.md](errors.md). -## Стек и границы +## Что решено -htmx-first: `gin` плюс `html/template` (рендер на сервере) плюс htmx. Ничего -сверх этого: **без шага сборки, без Node и сборщика, без реактивных -фреймворков**. htmx вендорится и раздаётся с нашего же хоста (`go:embed`, -`/static/vendor/`), без CDN. +- **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт + контракт данных, разметку собирает клиент. +- **Приложение ставится на телефон** и запускается с ярлыка: манифест, иконки, + service worker. +- **Статика вшивается в бинарник** через `go:embed` и раздаётся им же. Внешнего + веб-сервера под статику не заводим, бинарник остаётся самодостаточным. +- **Шрифты и скрипты — со своего хоста**, без внешних. Внешних ресурсов времени + выполнения нет. +- **Офлайн-чтения расшифровок и очереди отправки без сети не делаем** — граница + цели [web-access](../../tasks/items/web-access.md). Без сети приложение + показывает состояние, а не пустой экран. +- **Web Push не делаем**: уведомления идут через apprise и ntfy, цель + [ready-notification](../../tasks/items/ready-notification.md). +- **Записи звука в приложении не делаем** — файл выбирают системным диалогом. -- Свой JS сведён к минимуму: только то, чего серверу знать не нужно. - **Клиентского пересчёта доменного состояния нет** — состояние считает сервер, - клиент лишь подменяет присланную разметку. -- Alpine.js и SPA сознательно **не вводим**. Понадобится реактивный клиентский - виджет — вводим отдельным изменением и записываем решение, не раньше. +## Что не решено -## Единый источник разметки: партиал равен странице равен фрагменту - -Переиспользуемый кусок — это `{{define "name"}}` в каталоге партиалов. Тот же -`{{define}}` рендерится **и** внутри страницы (`{{template "name" .}}`), **и** -как ответ-фрагмент того же обработчика. Отдельной разметки под фрагмент не -заводим — иначе она разъедется со страницей. - -**Инвариант: корень `{{define}}` — это элемент с целевым `id`**, например -`#job-{id}`. `hx-swap="outerHTML"` заменяет весь корневой узел; если ответный -фрагмент не несёт тот же корневой `id`, следующее действие или опрос не найдёт -цель. Разметку и `id` держим в одном партиале. - -Сборку данных для шаблона выносим в отдельную функцию и зовём её и на полной -странице, и во фрагменте — чтобы htmx-ветка не копировала сборку. - -## Обработчик действия: ветвление htmx и редирект - -htmx-запрос определяем по заголовку `HX-Request: true`. - -Обработчик действия зовёт доменную операцию **одинаково** в обеих ветках, а -дальше ветвится: без htmx — привычный редирект после POST (303); с htmx — -перечитать актуальное состояние, собрать данные тем же сборщиком и отдать -фрагмент. - -Рендер именованного шаблона идёт **в буфер** и только затем пишется в ответ: при -ошибке шаблона клиент не получит полстраницы. - -## Деградация без JS обязательна - -Формы действий остаются обычными `