Files
transcriber/docs/conventions/web-ui.md
T
av b76f2d7c7e docs: раскладка переехала в .av-dev.toml, а расхождения документов сведены
- Перевод на канон 1 доделан: адреса служебного файла и имена скиллов
  переставлены в девяти местах прозы и кода, гейт зовёт три скрипта по новым
  путям, прежние docs/.docs.json и tasks/.tasks.json удалены.
- Сверка двумя агентами нашла четырнадцать расхождений, тринадцать сведены
  строками: число прогонов ревью и преамбула журнала дефектов, счёт capability,
  маршруты README, дубли инварианта захвата и кодов прогона, протухшие указатели
  записок разведки, маркер долга на переехавшем абзаце. Срок жизни сессии
  нормирует спека access, database.md на неё ссылается.
- Purpose спеки pipeline объявляет неописанным то, что в ней же и стоит; правка
  идёт изменением openspec, поэтому заведена задача pipeline-spec-purpose-drift.
2026-08-13 12:36:36 +03:00

8.7 KiB
Raw Blame History

Веб-UI

Конвенция: как мы пишем код приложения. Это правила оформления кода (How), а не спецификация поведения — что именно приложение показывает и какие действия обязано поддерживать, живёт в спеке OpenSpec.

Фреймворк выбран 2026-08-11 разведкой spa-framework-choice: ADR, сравнение кандидатов в research/spa-framework.md. Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята целиком вместе со сменой решения на SPA 2026-08-10.

Кода приложения ещё нет. Правила ниже выведены из выбора и из замера на пробном экране, а не из написанного кода: первым их применяет и проверяет spa-skeleton. Место, где правило разойдётся с тем, что окажется удобным, — повод править эту запись, а не обходить её молча.

Логирование запросов — logging.md. Трансляция доменных ошибок наружу — errors.md.

Механизировано: типы разметки и кода проверяет vue-tsc, и он входит в команду сборки, а не стоит отдельным шагом. Правил линтера для кода приложения пока нет.

Что решено про само приложение

  • Приложение — SPA, а не страницы, отрисованные сервером. Сервер отдаёт контракт данных, разметку собирает клиент.
  • Приложение ставится на телефон и запускается с ярлыка: манифест, иконки, service worker.
  • Статика вшивается в бинарник через go:embed и раздаётся им же. Внешнего веб-сервера под статику не заводим, бинарник остаётся самодостаточным.
  • Шрифты и скрипты — со своего хоста, без внешних. Внешних ресурсов времени выполнения нет.
  • Офлайн-чтения расшифровок и очереди отправки без сети не делаем — граница цели web-access. Без сети приложение показывает состояние, а не пустой экран.
  • Web Push не делаем: уведомления идут через apprise и ntfy, цель ready-notification.
  • Записи звука в приложении не делаем — файл выбирают системным диалогом.

Фреймворк и сборка

  • Vue 3, TypeScript, сборка Vite. Серверной отрисовки нет, надстройки над фреймворком (Nuxt) нет: она ждёт рядом процесс Node, а у нас статика в бинарнике.
  • Компонент — однофайловый, <script setup lang="ts">. Options API не пишем: два способа объявить компонент в одном приложении — второй способ делать то же самое.
  • Собранная статика неизменяема и адресуется хешем в имени. Имена придумывает Vite, руками их не задаём: от этого зависит обновление установленного приложения.
  • Шаг сборки входит в task gate и в сборку образа. Красная сборка статики роняет гейт наравне с go build.

Маршруты

  • Четыре экрана, одна таблица маршрутов через createRouter. Маршруты по файлам не включаем: сборочная надстройка роутера пятой версии стоит 34 пакета в установке и на четырёх маршрутах не окупается.
  • Адреса обычные, а не после решётки (createWebHistory). Отсюда требование к серверу: неизвестный путь вне /api/ отдаёт index.html, а не 404; пути внутри /api/ в приложение не проваливаются никогда.
  • Экран не знает, как он открыт. Данные экран берёт по своему адресу, а не получает от предыдущего: приложение открывают по ссылке и обновляют страницу посередине.

Состояние и обращение к API

  • Состояние экрана живёт в экранеref и computed по месту. Общее между экранами выносим в composable-функцию use….
  • Хранилища состояния (Pinia) не заводим, пока два экрана не потребуют одних и тех же данных одновременно. Заведём — это правка этой записи с названной причиной.
  • Обращение к API — через fetch и через одну свою обёртку. Сторонних клиентов (axios и подобных) не берём: внешних ресурсов у нас нет, а разбор ответа и отображение ошибки всё равно свои.
  • Обёртка — единственное место, где читается код ответа. Она же превращает ошибку контракта в доменную ошибку приложения; экран получает готовый текст, а не Response.
  • Сессия живёт кукой transcriber_session, и приложение её не читает: кука HttpOnly, браузер шлёт её сам, а вошедшего экран узнаёт по ответу API. Норма — access.

Показ ошибок и состояний

  • Текст ошибки приходит с сервера и показывается как есть. Своих текстов под коды ответа приложение не сочиняет: единая форма ошибки — обязанность API (json-api-for-spa), и второй словарь на клиенте разошёлся бы с первым.
  • Отсутствие связи — состояние, а не ошибка. Сорванный запрос показывается строкой «связи нет», а не пустым экраном и не сообщением браузера.
  • У каждого списка три состояния и все три нарисованы: загружается, пусто, есть данные. Пустой список без надписи неотличим от незагруженного.
  • Текст, который видит пользователь, — русский (CLAUDE.md, «Язык»). Код и идентификаторы английские, включая имена компонентов и файлов.

Что не решено

  • Набор компонентов и стили. Своя разметка или готовый набор — не решено, а готовый способен удвоить собранный файл (research/spa-framework.md, «Чего разведка не узнала»).
  • Устройство service worker и версионирование статики — задача installable-pwa.
  • Как связываются пользователь Telegram и пользователь веба — открытый вопрос «Учётные записи» в ../architecture.md.