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

118 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Веб-UI
Конвенция: *как* мы пишем код приложения. Это правила оформления кода (How), а не
спецификация поведения — что именно приложение показывает и какие действия
обязано поддерживать, живёт в спеке OpenSpec.
Фреймворк выбран 2026-08-11 разведкой `spa-framework-choice`:
[ADR](../adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в
[research/spa-framework.md](../research/spa-framework.md). Прежняя редакция
описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята
целиком вместе со сменой решения на SPA 2026-08-10.
**Кода приложения ещё нет.** Правила ниже выведены из выбора и из замера на
пробном экране, а не из написанного кода: первым их применяет и проверяет
`spa-skeleton`. Место, где правило разойдётся с тем, что окажется удобным, —
повод править эту запись, а не обходить её молча.
Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок
наружу — [errors.md](errors.md).
**Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в
команду сборки, а не стоит отдельным шагом. Правил линтера для кода приложения
пока нет.
## Что решено про само приложение
- **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт
контракт данных, разметку собирает клиент.
- **Приложение ставится на телефон** и запускается с ярлыка: манифест, иконки,
service worker.
- **Статика вшивается в бинарник** через `go:embed` и раздаётся им же. Внешнего
веб-сервера под статику не заводим, бинарник остаётся самодостаточным.
- **Шрифты и скрипты — со своего хоста**, без внешних. Внешних ресурсов времени
выполнения нет.
- **Офлайн-чтения расшифровок и очереди отправки без сети не делаем** — граница
из [паспорта](../passport.md). Без сети приложение показывает состояние, а не
пустой экран.
- **Web Push не делаем**: уведомления идут через apprise и ntfy — решение живёт
в [architecture.md](../architecture.md), «Уведомления», делает его
[ntfy-delivery](../../tasks/items/ntfy-delivery.md).
- **Записи звука в приложении не делаем** — граница из
[паспорта](../passport.md), «Диктофон»; файл выбирают системным диалогом.
## Фреймворк и сборка
- **Vue 3, TypeScript, сборка Vite.** Серверной отрисовки нет, надстройки над
фреймворком (Nuxt) нет: она ждёт рядом процесс Node, а у нас статика в
бинарнике.
- **Компонент — однофайловый, `<script setup lang="ts">`.** Options API не
пишем: два способа объявить компонент в одном приложении — второй способ
делать то же самое.
- **Собранная статика неизменяема и адресуется хешем в имени.** Имена придумывает
Vite, руками их не задаём: от этого зависит обновление установленного
приложения.
- **Шаг сборки входит в `task gate` и в сборку образа.** Красная сборка статики
роняет гейт наравне с `go build`.
## Маршруты
- **Одна таблица маршрутов** через `createRouter`. Маршруты по файлам не
включаем: сборочная надстройка роутера пятой версии стоит 34 пакета в
установке и на нашем числе маршрутов не окупается
([research/spa-framework.md](../research/spa-framework.md), «Vue»). Сколько
экранов и какие — не здесь: состав нормирует спека приложения, а до неё его
держит [spa-skeleton](../../tasks/items/spa-skeleton.md).
- **Адреса обычные, а не после решётки** (`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](../../openspec/specs/access/spec.md).
## Показ ошибок и состояний
- **Текст ошибки приходит с сервера и показывается как есть.** Своих текстов под
коды ответа приложение не сочиняет: единая форма ошибки — обязанность API
(спека [archive](../../openspec/specs/archive/spec.md)), и второй словарь
на клиенте разошёлся бы с первым.
- **Отсутствие связи — состояние, а не ошибка.** Сорванный запрос показывается
строкой «связи нет», а не пустым экраном и не сообщением браузера.
- **У каждого списка три состояния и все три нарисованы:** загружается, пусто,
есть данные. Пустой список без надписи неотличим от незагруженного.
- **Текст, который видит пользователь, — русский** ([CLAUDE.md](../../CLAUDE.md),
«Язык»). Код и идентификаторы английские, включая имена компонентов и файлов.
## Что не решено
- **Набор компонентов и стили.** Своя разметка или готовый набор — не решено, а
готовый способен удвоить собранный файл
([research/spa-framework.md](../research/spa-framework.md), «Чего разведка не
узнала»).
- **Устройство service worker и версионирование статики** — задача
[installable-pwa](../../tasks/items/installable-pwa.md).
- **Как связать чат Telegram с учётной записью** — открытый вопрос; от него зависит возвращение убранного 2026-08-14 входа
«Учётные записи» в [../architecture.md](../architecture.md).