13 KiB
Веб-UI
Конвенция: как мы пишем код приложения. Это правила оформления кода (How), а не спецификация поведения — что именно приложение показывает и какие действия обязано поддерживать, живёт в спеке OpenSpec.
Фреймворк выбран 2026-08-11 разведкой spa-framework-choice:
ADR, сравнение кандидатов в
research/spa-framework.md. Прежняя редакция
описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята
целиком вместе со сменой решения на SPA 2026-08-10.
Правила ниже применены каркасом приложения (spa-skeleton, 2026-08-15): до него
они были выведены из выбора и из замера на пробном экране, а не из написанного
кода. Место, где правило разойдётся с тем, что окажется удобным, — повод править
эту запись, а не обходить её молча.
Логирование запросов — logging.md. Трансляция доменных ошибок наружу — errors.md.
Механизировано: типы разметки и кода проверяет vue-tsc, и он входит в
команду сборки, а не стоит отдельным шагом. Форматирование и статический анализ
держит Biome, поведение экранов — юнит-тесты Vue; оба шага входят в набор
проверок наравне со сборкой. Инструменты зовутся контейнером, а не из PATH:
требованием к машине разработчика остаётся docker, а не установленный Node.
Что решено про само приложение
- Приложение — 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»). Сколько экранов и какие — не здесь: состав нормирует спека webapp. - Адреса обычные, а не после решётки (
createWebHistory). Отсюда требование к серверу: неизвестный путь вне корней сервиса отдаётindex.html, а не404; путь внутри корня в приложение не проваливается никогда. Корней сегодня четыре —/api/у хранилища,/app/у приложения,/auth/у входа,/_/у панели, — плюс/healthи/metricsотдельными адресами. Приложение уехало из общего/api/решением владельца 2026-08-15: пространство принадлежит хранилищу, и обновление библиотеки вправе занять там имя рядом с нашим. Перечень корней сервису не описывают, а из него порождают регистрацию маршрутов: описанный порознь, он разошёлся бы с ними молча. - Несовпавший ресурс разметкой не подменяется. Путь под каталогом сборщика,
которому не нашлось файла, отвечает
404. Правило — вторая половина предыдущего: разметка прежней сборки называет ресурсы прежней сборки, и подменить их разметкой значит ответить200на то, чего нет. Браузер отвергнет такой ответ по типу содержимого, человек увидит пустой экран, а в кодах ответов сервиса не останется ничего. - Экран не знает, как он открыт. Данные экран берёт по своему адресу, а не получает от предыдущего: приложение открывают по ссылке и обновляют страницу посередине.
Состояние и обращение к API
- Состояние экрана живёт в экране —
refиcomputedпо месту. Общее между экранами выносим в composable-функциюuse…. - Хранилища состояния (Pinia) не заводим, пока два экрана не потребуют одних и тех же данных одновременно. Заведём — это правка этой записи с названной причиной.
- Обращение к API — через
fetchи через одну свою обёртку. Сторонних клиентов (axios и подобных) не берём: внешних ресурсов у нас нет, а разбор ответа и отображение ошибки всё равно свои. - Обёртка — единственное место, где читается код ответа. Она же превращает
ошибку контракта в доменную ошибку приложения; экран получает готовый текст, а
не
Response. - Сессия живёт кукой
transcriber_session, и приложение её не читает: кукаHttpOnly, браузер шлёт её сам, а вошедшего экран узнаёт по ответу API. Норма — access.
Показ ошибок и состояний
- Текст ошибки приходит с сервера и показывается как есть. Своих текстов под коды ответа приложение не сочиняет: единая форма ошибки — обязанность API (спека archive), и второй словарь на клиенте разошёлся бы с первым.
- Отсутствие связи — состояние, а не ошибка. Сорванный запрос показывается строкой «связи нет», а не пустым экраном и не сообщением браузера.
- У каждого списка три состояния и все три нарисованы: загружается, пусто, есть данные. Пустой список без надписи неотличим от незагруженного.
- Текст, который видит пользователь, — русский (CLAUDE.md, «Язык»). Код и идентификаторы английские, включая имена компонентов и файлов.
Что не решено
- Инструмент статического анализа проверен наполовину. Biome взят решением владельца 2026-08-15 и разбирает однофайловые компоненты; замены он потребует, если перестанет их держать. Тогда это отдельное решение, а не подстановка по ходу.
- Проверка типов держится на пятой линии TypeScript. С седьмой
vue-tscне работает: новый компилятор не отдаёт точку входа, которую тот зовёт. Проверено прогоном 2026-08-15. - Набор компонентов и стили. Своя разметка или готовый набор — не решено, а готовый способен удвоить собранный файл (research/spa-framework.md, «Чего разведка не узнала»).
- Устройство service worker и версионирование статики — задача installable-pwa.
- Как связать чат Telegram с учётной записью — открытый вопрос; от него зависит возвращение убранного 2026-08-14 входа «Учётные записи» в ../architecture.md.