# Веб-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`, 2026-08-15): до него
они были выведены из выбора и из замера на пробном экране, а не из написанного
кода. Место, где правило разойдётся с тем, что окажется удобным, — повод править
эту запись, а не обходить её молча.

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

**Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в
команду сборки, а не стоит отдельным шагом. Форматирование и статический анализ
держит **Biome**, поведение экранов — **юнит-тесты Vue**; оба шага входят в набор
проверок наравне со сборкой. Инструменты зовутся контейнером, а не из `PATH`:
требованием к машине разработчика остаётся docker, а не установленный Node.

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

- **Приложение — 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»). Сколько
  экранов и какие — не здесь: состав нормирует спека
  [webapp](../../openspec/specs/webapp/spec.md).
- **Адреса обычные, а не после решётки** (`createWebHistory`). Отсюда требование
  к серверу: неизвестный путь **вне корней сервиса** отдаёт `index.html`, а не
  `404`; путь внутри корня в приложение не проваливается никогда. Корень
  сегодня **один** — `/app/` у приложения, — плюс `/health` и `/metrics`
  отдельными адресами. Корни `/auth/`, `/api/` и `/_/` сняты 2026-08-22: первый
  ушёл с собственным входом, два других — со встроенным хранилищем и его
  панелью, и пути под ними стали обычными путями вне корней. Приложение уехало
  из общего `/api/` решением владельца 2026-08-15, и корень свой сохранило:
  соседа, ради которого выбирался, больше нет, а формы запросов и ответов от
  смены хранилища не изменились ни одним полем. Перечень корней сервису не
  описывают, а из него **порождают** регистрацию маршрутов: описанный порознь,
  он разошёлся бы с ними молча.
- **Несовпавший ресурс разметкой не подменяется.** Путь под каталогом сборщика,
  которому не нашлось файла, отвечает `404`. Правило — вторая половина
  предыдущего: разметка прежней сборки называет ресурсы прежней сборки, и
  подменить их разметкой значит ответить `200` на то, чего нет. Браузер отвергнет
  такой ответ по типу содержимого, человек увидит пустой экран, а в кодах
  ответов сервиса не останется ничего.
- **Экран не знает, как он открыт.** Данные экран берёт по своему адресу, а не
  получает от предыдущего: приложение открывают по ссылке и обновляют страницу
  посередине.

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

- **Состояние экрана живёт в экране** — `ref` и `computed` по месту. Общее между
  экранами выносим в composable-функцию `use…`.
- **Хранилища состояния (Pinia) не заводим**, пока два экрана не потребуют одних
  и тех же данных одновременно. Заведём — это правка этой записи с названной
  причиной.
- **Обращение к API — через `fetch` и через одну свою обёртку.** Сторонних
  клиентов (axios и подобных) не берём: внешних ресурсов у нас нет, а разбор
  ответа и отображение ошибки всё равно свои.
- **Обёртка — единственное место, где читается код ответа.** Она же превращает
  ошибку контракта в доменную ошибку приложения; экран получает готовый текст, а
  не `Response`.
- **Сессии у сервиса нет вовсе**, и приложение не хранит ничего: кто пришёл,
  называет заголовок обратного прокси, а приложение узнаёт его ответом API.
  Кук сервис не ставит — это свойство сторожится проверкой. Норма —
  [access](../../openspec/specs/access/spec.md), решение —
  [ADR-2026-08-22-login-by-trusted-header](../adr/ADR-2026-08-22-login-by-trusted-header.md).

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

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

## Что не решено

- **Инструмент статического анализа проверен наполовину.** Biome взят решением
  владельца 2026-08-15 и разбирает однофайловые компоненты; замены он потребует,
  если перестанет их держать. Тогда это отдельное решение, а не подстановка по
  ходу.
- **Проверка типов держится на пятой линии TypeScript.** С седьмой `vue-tsc`
  не работает: новый компилятор не отдаёт точку входа, которую тот зовёт.
  Проверено прогоном 2026-08-15.
- **Набор компонентов и стили.** Своя разметка или готовый набор — не решено, а
  готовый способен удвоить собранный файл
  ([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).
