Files
transcriber/docs/conventions/web-ui.md
T
av c9b7765646 хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose
  под файловым замком, одна миграция начальной схемы вместо семи прежних
- транспорт переписан на net/http: свои слои, свой ограничитель частоты,
  отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли
- по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета
  читается справа налево, узнавание известного идёт читающим пулом
2026-08-23 08:06:04 +03:00

146 lines
13 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`, 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).