Files
transcriber/docs/conventions/web-ui.md
T

9.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 Push не делаем: уведомления идут через apprise и ntfy — решение живёт в architecture.md, «Уведомления», делает его ntfy-delivery.
  • Записи звука в приложении не делаем — граница из паспорта, «Диктофон»; файл выбирают системным диалогом.

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

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

Маршруты

  • Одна таблица маршрутов через createRouter. Маршруты по файлам не включаем: сборочная надстройка роутера пятой версии стоит 34 пакета в установке и на нашем числе маршрутов не окупается (research/spa-framework.md, «Vue»). Сколько экранов и какие — не здесь: состав нормирует спека приложения, а до неё его держит spa-skeleton.
  • Адреса обычные, а не после решётки (createWebHistory). Отсюда требование к серверу: неизвестный путь вне корней сервиса отдаёт index.html, а не 404; путь внутри корня в приложение не проваливается никогда. Корней сегодня четыре — /api/ у хранилища, /app/ у приложения, /auth/ у входа, /_/ у панели, — плюс /health и /metrics отдельными адресами. Приложение уехало из общего /api/ решением владельца 2026-08-15: пространство принадлежит хранилищу, и обновление библиотеки вправе занять там имя рядом с нашим.
  • Экран не знает, как он открыт. Данные экран берёт по своему адресу, а не получает от предыдущего: приложение открывают по ссылке и обновляют страницу посередине.

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

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

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

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

Что не решено

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