docs: фреймворком приложения выбран Vue 3 с роутером 5 и сборкой Vite

- заведены записка разведки docs/research/spa-framework.md со сравнением Svelte,
  Vue и React на одном экране и решение ADR-2026-08-11-spa-on-vue
- docs/conventions/web-ui.md переписан под Vue: компоненты, маршруты, состояние,
  обращение к API и показ ошибок
- закрыт вопрос «Приложение» в docs/architecture.md, уточнена задача spa-skeleton
This commit is contained in:
av
2026-08-11 14:51:55 +03:00
parent f1524fefd8
commit 54ca4c0e50
8 changed files with 324 additions and 29 deletions
+76
View File
@@ -0,0 +1,76 @@
# Приложение пишем на Vue, а Node входит в гейт и в образ
- **Дата:** 2026-08-11
- **Источник:** [../research/spa-framework.md](../research/spa-framework.md) —
записка разведки `spa-framework-choice`
## Решение
Приложение пишем на **Vue 3** с роутером пятой версии и собираем **Vite** в
статику, которую бинарник вшивает через `go:embed` и раздаёт сам. Маршруты
задаём своей таблицей через `createRouter`; сборочную надстройку роутера под
маршруты по файлам не включаем.
Вместе с этим в проект входит **шаг сборки статики**: Node и `npm` становятся
нужны на машине разработчика, отдельным шагом в `task gate` и слоем сборки в
`Dockerfile`.
Это два решения, а не одно, но принимаются они вместе: шаг сборки появляется при
любом из трёх кандидатов, и отдельно от выбора фреймворка его обсуждать не о чем.
## Почему Vue
Разведка мерила три кандидата на одном и том же экране и нашла единственное
различие, которое расходится в разы:
> Различает единственное — **размер того, что скачивает телефон**, и он
> расходится вчетверо.
Вшивание в бинарник, цена шага сборки в гейте и установка на телефон у всех трёх
оказались одинаковыми и потому ничего не решают.
По размеру Vue стоит посередине — 33 326 Б на четыре экрана против 17 314 у
Svelte и 72 402 у React. Выбран он не по этому числу, а по устойчивости
экосистемы, и оба отвергнутых кандидата отвергнуты с названной ценой:
> **Svelte** — легче Vue вдвое, но своего роутера не имеет, а тот, что есть,
> держит один человек. Владелец выбрал экосистему, которая переживёт проект, а не
> минимальный размер: 33 КБ на телефоне не отличаются от 17 КБ на глаз, а
> брошенная зависимость отличается.
>
> **React** — вчетверо тяжелее Svelte и вдвое тяжелее Vue, а взамен даёт
> экосистему, которой приложению на четыре экрана не на что потратиться.
Роутер берём пятой версии, а не четвёртой, по тому же доводу: она стабильна с
29 января 2026 и несёт метку `latest`, то есть чинить будут её, а не
предшественницу. Её сборочная надстройка добавляет 34 пакета в установку, и это
принятая цена; на собранный файл она не влияет и необязательна.
## Почему это ADR
Запись проходит триггер **дорогим откатом**: переход на другой фреймворк
переписывает все экраны разом, а не один файл. Шаг сборки сюда же — он меняет
требования к машине разработчика и к образу, и снять его потом можно только
вместе с приложением.
Прежнего решения запись не пересматривает: htmx был снят решением о SPA от
2026-08-10, до заведения этого журнала, и парного статуса «заменено на» ставить
нечему.
## Последствия
- `+` разметка отделена от кода однофайловым компонентом, а роутер и хранилище
состояния идут из тех же рук, что и сам фреймворк: третьей библиотеки под них
заводить не нужно.
- `+` собранная статика — три файла и значок, поэтому `go:embed` берёт каталог
обычной строкой, а бинарник остаётся самодостаточным.
- `` **гейт перестаёт зависеть только от Go.** Красный шаг сборки статики
становится таким же поводом остановиться, как красный `go build`, а машина
разработчика получает второе требуемое окружение сверх `ffmpeg`.
- `` **в образ добавляется слой Node** ради шага, результат которого — три
файла; насколько дольше собирается образ и насколько тяжелеет, не замерялось.
- `` в проект приходит `node_modules` на 92 МБ и 84 пакета, за которыми надо
следить отдельно от зависимостей Go: `gitleaks` и `golangci-lint` про них
ничего не знают.
- `` приложение весит 33 КБ там, где на Svelte весило бы 17. Разница куплена
сознательно и обратно не отыгрывается.
+1
View File
@@ -32,6 +32,7 @@
| Дата | Запись | Статус | | Дата | Запись | Статус |
| --- | --- | --- | | --- | --- | --- |
| 2026-08-11 | [Приложение пишем на Vue, а Node входит в гейт и в образ](ADR-2026-08-11-spa-on-vue.md) | |
| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | | | 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | |
| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | | | 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | |
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | | | 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
+7 -5
View File
@@ -134,11 +134,13 @@
администратора при этом Authelia не закрывает: у неё свой пароль администратора при этом Authelia не закрывает: у неё свой пароль
суперпользователя. суперпользователя.
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA, - **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
устанавливаемое на телефон; фреймворк выбирает разведка устанавливаемое на телефон, а фреймворком взят Vue 3 с роутером пятой версии и
`spa-framework-choice`, и до её итога сборкой Vite — 2026-08-11,
[conventions/web-ui.md](conventions/web-ui.md) стоит почти пустой. Шаг сборки [ADR](adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в
фронтенда меняет требования к машине разработчика и к образу — решение уровня [research/spa-framework.md](research/spa-framework.md). Тем же решением Node
ADR. входит в гейт и слоем в сборку образа. Пишет это `spa-skeleton`; во что
обходится слой Node в образе, не замерялось. Не решено, брать ли готовый набор
компонентов.
- **Уведомления.** Пользователь веба узнаёт о готовности только опросом. - **Уведомления.** Пользователь веба узнаёт о готовности только опросом.
Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web
Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и
+3 -4
View File
@@ -39,10 +39,9 @@ htmx, а здесь решено делать SPA — и перенесённы
`0600`, самодокументируемый `config.dist.toml`, проверка на старте. `0600`, самодокументируемый `config.dist.toml`, проверка на старте.
- [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT - [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT
ULID, разбор на входной границе, естественные ключи у деталей. ULID, разбор на входной границе, естественные ключи у деталей.
- [web-ui.md](web-ui.md) — веб-UI: что решено про приложение (SPA, установка на - [web-ui.md](web-ui.md) — веб-UI: Vue 3 с Vite и статикой в бинарнике,
телефон, статика в бинарнике) и что ждёт выбора фреймворка. **Почти пуста:** однофайловые компоненты, таблица маршрутов, состояние в экране, одна обёртка
редакция на htmx снята 2026-08-10 вместе со сменой решения, а новую писать не над `fetch`, показ ошибок и состояний списка.
под что до разведки `spa-framework-choice`.
## Механизировано ## Механизировано
+75 -12
View File
@@ -4,19 +4,25 @@
спецификация поведения — что именно приложение показывает и какие действия спецификация поведения — что именно приложение показывает и какие действия
обязано поддерживать, живёт в спеке OpenSpec. обязано поддерживать, живёт в спеке OpenSpec.
**Фреймворк не выбран, и до выбора эта конвенция пуста.** Здесь стоит только то, Фреймворк выбран 2026-08-11 разведкой `spa-framework-choice`:
что решено и от фреймворка не зависит; всё остальное появится по итогу разведки [ADR](../adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в
`spa-framework-choice`, которая же заведёт запись в `research/` и ADR. [research/spa-framework.md](../research/spa-framework.md). Прежняя редакция
описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята
целиком вместе со сменой решения на SPA 2026-08-10.
Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные **Кода приложения ещё нет.** Правила ниже выведены из выбора и из замера на
фреймворки. Решение сменилось на SPA 2026-08-10, и та редакция снята целиком: пробном экране, а не из написанного кода: первым их применяет и проверяет
частичная подмена фрагментов, ветвление по `HX-Request` и деградация без JS к `spa-skeleton`. Место, где правило разойдётся с тем, что окажется удобным, —
SPA не относятся ни одним пунктом. повод править эту запись, а не обходить её молча.
Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок
наружу — [errors.md](errors.md). наружу — [errors.md](errors.md).
## Что решено **Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в
команду сборки, а не стоит отдельным шагом. Правил линтера для кода приложения
пока нет.
## Что решено про само приложение
- **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт - **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт
контракт данных, разметку собирает клиент. контракт данных, разметку собирает клиент.
@@ -33,9 +39,66 @@ SPA не относятся ни одним пунктом.
[ready-notification](../../tasks/items/ready-notification.md). [ready-notification](../../tasks/items/ready-notification.md).
- **Записи звука в приложении не делаем** — файл выбирают системным диалогом. - **Записи звука в приложении не делаем** — файл выбирают системным диалогом.
## Фреймворк и сборка
- **Vue 3, TypeScript, сборка Vite.** Серверной отрисовки нет, надстройки над
фреймворком (Nuxt) нет: она ждёт рядом процесс Node, а у нас статика в
бинарнике.
- **Компонент — однофайловый, `<script setup lang="ts">`.** Options API не
пишем: два способа объявить компонент в одном приложении — второй способ
делать то же самое.
- **Собранная статика неизменяема и адресуется хешем в имени.** Имена придумывает
Vite, руками их не задаём: от этого зависит обновление установленного
приложения.
- **Шаг сборки входит в `task gate` и в сборку образа.** Красная сборка статики
роняет гейт наравне с `go build`.
## Маршруты
- **Четыре экрана, одна таблица маршрутов** через `createRouter`. Маршруты по
файлам не включаем: сборочная надстройка роутера пятой версии стоит 34 пакета
в установке и на четырёх маршрутах не окупается.
- **Адреса обычные, а не после решётки** (`createWebHistory`). Отсюда требование
к серверу: неизвестный путь **вне** `/api/` отдаёт `index.html`, а не `404`;
пути внутри `/api/` в приложение не проваливаются никогда.
- **Экран не знает, как он открыт.** Данные экран берёт по своему адресу, а не
получает от предыдущего: приложение открывают по ссылке и обновляют страницу
посередине.
## Состояние и обращение к API
- **Состояние экрана живёт в экране** — `ref` и `computed` по месту. Общее между
экранами выносим в composable-функцию `use…`.
- **Хранилища состояния (Pinia) не заводим**, пока два экрана не потребуют одних
и тех же данных одновременно. Заведём — это правка этой записи с названной
причиной.
- **Обращение к API — через `fetch` и через одну свою обёртку.** Сторонних
клиентов (axios и подобных) не берём: внешних ресурсов у нас нет, а разбор
ответа и отображение ошибки всё равно свои.
- **Обёртка — единственное место, где читается код ответа.** Она же превращает
ошибку контракта в доменную ошибку приложения; экран получает готовый текст, а
не `Response`.
## Показ ошибок и состояний
- **Текст ошибки приходит с сервера и показывается как есть.** Своих текстов под
коды ответа приложение не сочиняет: единая форма ошибки — обязанность API
([json-api-for-spa](../../tasks/items/json-api-for-spa.md)), и второй словарь
на клиенте разошёлся бы с первым.
- **Отсутствие связи — состояние, а не ошибка.** Сорванный запрос показывается
строкой «связи нет», а не пустым экраном и не сообщением браузера.
- **У каждого списка три состояния и все три нарисованы:** загружается, пусто,
есть данные. Пустой список без надписи неотличим от незагруженного.
- **Текст, который видит пользователь, — русский** ([CLAUDE.md](../../CLAUDE.md),
«Язык»). Код и идентификаторы английские, включая имена компонентов и файлов.
## Что не решено ## Что не решено
Фреймворк, форма сборки, способ хранения состояния на клиенте, правила - **Набор компонентов и стили.** Своя разметка или готовый набор — не решено, а
разбиения на компоненты, обращение к API и показ ошибок. Всё это — предмет готовый способен удвоить собранный файл
`spa-framework-choice`; писать их наперёд, не зная фреймворка, значит написать ([research/spa-framework.md](../research/spa-framework.md), «Чего разведка не
правила, которые придётся выбросить второй раз. узнала»).
- **Устройство service worker и версионирование статики** — задача
[installable-pwa](../../tasks/items/installable-pwa.md).
- **Где живёт сессия и как приложение узнаёт вошедшего** — открытый вопрос
«Учётные записи» в [../architecture.md](../architecture.md).
+3 -1
View File
@@ -9,7 +9,8 @@
## Как снималось ## Как снималось
На живом потоке не снималось ничего: поведение внешних сервисов на границах не На живом потоке не снималось ничего: поведение внешних сервисов на границах не
проверяли. Обе записи сделаны в песочнице на пустой базе. проверяли. Все записи сделаны в песочнице на пустой базе либо в каталоге вне
репозитория.
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
@@ -21,5 +22,6 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
| Дата | Запись | О чём | | Дата | Запись | О чём |
| --- | --- | --- | | --- | --- | --- |
| 2026-08-11 | [Фреймворк приложения: Svelte, Vue и React на одном экране](spa-framework.md) | Размер собранной статики, цена шага сборки, что у трёх кандидатов одинаково |
| 2026-08-11 | [Очередь задач: своя таблица против готовой библиотеки](job-queue.md) | Цена River и goqite в пакетах, захват одним запросом, чего нет для PocketBase | | 2026-08-11 | [Очередь задач: своя таблица против готовой библиотеки](job-queue.md) | Цена River и goqite в пакетах, захват одним запросом, чего нет для PocketBase |
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 | | 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
+139
View File
@@ -0,0 +1,139 @@
# Фреймворк приложения: Svelte, Vue и React на одном экране
Отвечает на вопрос разведки `spa-framework-choice`: какой фреймворк берём под
приложение на четыре экрана, которое собирается в статику, вшивается в бинарник
через `go:embed` и ставится на телефон.
Кандидатов назвал владелец: Svelte, Vue и React, все с Vite. Мера тоже названа им
— размер собранной статики, простота вшивания и цена шага сборки в гейте, а не
популярность. Серверную отрисовку и надстройки над фреймворками — SvelteKit,
Nuxt, Next — не рассматривали: конвенция
[../conventions/web-ui.md](../conventions/web-ui.md) уже требует статику в
бинарнике, а все три надстройки по умолчанию ждут процесс Node рядом.
## Как снималось
Дата замеров — 2026-08-11. Всё считал в каталоге вне репозитория, который удалён
вместе с песочницей. Node 24.18.0, npm 11.16.0, Vite 8.2.1, TypeScript 6.0.3.
- **Каркасы** — `npm create vite@latest <имя> -- --template svelte-ts|vue-ts|react-ts`.
Из каждого удалил демонстрационные картинки и компонент-счётчик, чтобы в сборку
попал только пробный экран.
- **Пробный экран** один и тот же по смыслу: список записей, опрос состояния
незавершённых раз в две секунды, полоса ошибки, пустое состояние, разбор даты.
60 строк на Vue, 64 на Svelte, 69 на React; стили — один и тот же файл на 15
правил, и в сборке он у всех троих совпал до байта (883 Б), что и подтверждает
одинаковость экрана.
- **Четыре маршрута** — тот же экран плюс три заглушки и переходы между ними:
столько экранов у цели [web-access](../../tasks/items/web-access.md). Роутеры
`svelte-spa-router` 5.1.1, `vue-router` 5.2.0 и 4.6.4, `react-router` 8.3.0.
- **Размеры** — `stat -c%s` и `gzip -9c | wc -c` по файлам `dist/`. Числа Vite в
своём выводе печатает по другому уровню сжатия, поэтому в таблицах ниже стоят
мои.
- **Установка** — `npm ci --cache <свой пустой каталог>`; у каждого каркаса кэш
свой, иначе первый прогон скачивает общие пакеты за остальных.
- **Сборка** — `npm run build` трижды подряд с удалением `dist` и кэша Vite,
в таблице лучшее из трёх. Числа сняты на машине разработчика, не в гейте.
- **Совместимость `svelte-spa-router` со Svelte 5** взята из его описания в
реестре, а не проверена: `peerDependencies` объявляет `svelte: ^5.0.0`.
## Числа
| Мера | Svelte 5.56.8 | Vue 3.5.41 | React 19.2.8 |
| --- | --- | --- | --- |
| Пробный экран, скрипт | 35 598 Б / 13 931 Б gzip | 62 493 / 24 372 | 191 797 / 59 679 |
| Четыре маршрута с роутером | 45 053 / **17 314** | 86 890 / **33 326** | 228 750 / **72 402** |
| Стили, у всех один файл | 883 / 474 | 883 / 474 | 883 / 474 |
| Файлов в `dist/` | 3 плюс значок | то же | то же |
| Пакетов в установке, каркас | 49 | 48 | 27 |
| Пакетов с роутером | 51 | 84 (роутер 5) / 50 (роутер 4) | 29 |
| `node_modules` с роутером | 74 МБ | 92 МБ | 91 МБ |
| Установка с пустым кэшем | 7,4 с | 9,1 с | 9,4 с |
| `npm ci` с тёплым кэшем | 0,44 с | 0,41 с | 0,39 с |
| Сборка и проверка типов | 0,32 с плюс 1,16 с | 1,08 с | 0,81 с |
Пакеты считал так: имена первого уровня в `node_modules` плюс имена второго
уровня внутри областей `@…`. В `package-lock.json` записей больше — 74, 72 и 69
у каркасов, — потому что он перечисляет двоичные сборки Rollup и oxlint под все
платформы, а ставится одна.
Проверка типов у Vue и React входит в `npm run build` (`vue-tsc -b && vite build`
и `tsc -b && vite build`), у Svelte вынесена в отдельную команду `npm run check`
и в сборке не участвует — отсюда две цифры в последней строке.
## Что оказалось одинаковым и потому ничего не выбирает
- **Вшивание в бинарник.** У всех троих `dist/` — это `index.html`, один файл
скрипта и один файл стилей с хешем в имени плюс значок. Ни один не кладёт
файлов, начинающихся с точки или подчёркивания, поэтому `go:embed` берёт
каталог обычной строкой, без `all:`.
- **Node в гейте и в образе.** Шаг сборки статики нужен всем троим одинаково: на
машине разработчика, в `task gate` и слоем сборки в `Dockerfile`.
- **Установка на телефон.** `vite-plugin-pwa` 1.3.0 от фреймворка не зависит: в
его `peerDependencies` стоит Vite, и ни одного фреймворка там нет.
- **Цена шага сборки.** Секунда с небольшим у всех троих, и на фоне сборки Go и
`golangci-lint` в гейте это не различие.
Различает единственное — **размер того, что скачивает телефон**, и он расходится
вчетверо.
## Кандидаты
### Svelte
Компилятор, а не библиотека времени выполнения: в собранный файл попадает почти
только свой код, отсюда 17 314 Б на четыре экрана — вчетверо меньше React.
Реактивность и хранилище состояния встроены, третьей библиотеки под них не нужно.
Против: своего роутера у Svelte нет, а `svelte-spa-router` держит один человек.
Проверка типов идёт отдельной командой, то есть в гейте это второй шаг.
### Vue
Библиотека с официальным роутером и официальным хранилищем состояния. 33 326 Б на
четыре экрана — вдвое легче React и вдвое тяжелее Svelte. Разметка отделена от
кода однофайловым компонентом, документация переведена на русский.
Пятая версия роутера тянет в установку 34 пакета сверх четвёртой (84 против 50):
в неё встроена сборочная надстройка под маршруты по файлам. На собранный файл это
не влияет — 33 326 Б против 33 848 Б у четвёртой версии, то есть пятая даже чуть
легче, — и **надстройка не обязательна**: замер шёл на своей таблице маршрутов
через `createRouter`, ни один плагин Vite для этого не регистрировался.
Пятая версия — стабильная, а не предварительная: 5.0.0 вышла 29 января 2026,
текущая 5.2.0 — 15 июля, метка `latest` стоит на ней.
### React
Экосистема больше, чем у двух других, — числом я её не мерил, — а пакетов в
установке меньше всех: 27. Всё остальное против: 72 402 Б на четыре экрана, и ниже этого пола он не опускается, потому что
пол задаёт сама библиотека. Роутер, хранилище состояния и работа с запросами —
третьими библиотеками, каждая со своим сроком жизни.
## Что решено и от чего отказались
Решение — **Vue с роутером пятой версии**, записано в
[ADR-2026-08-11-spa-on-vue](../adr/ADR-2026-08-11-spa-on-vue.md).
Отвергнуты:
- **Svelte** — легче Vue вдвое, но своего роутера не имеет, а тот, что есть,
держит один человек. Владелец выбрал экосистему, которая переживёт проект, а не
минимальный размер: 33 КБ на телефоне не отличаются от 17 КБ на глаз, а
брошенная зависимость отличается;
- **React** — вчетверо тяжелее Svelte и вдвое тяжелее Vue, а взамен даёт
экосистему, которой приложению на четыре экрана не на что потратиться: чужих
компонентов оно не берёт, весь показ данных — список, форма загрузки и текст.
## Чего разведка не узнала
- **Как числа изменятся на настоящих экранах.** Мерил один экран и три заглушки;
загрузка файла с полосой хода, форма настроек и таблица расхода вырастут у всех
трёх. Переносится отношение, а не абсолютные значения.
- **Цену готовых наборов компонентов.** Не мерил вовсе, а именно она способна
удвоить собранный файл.
- **Во что обходится шаг сборки в образе.** Слой Node в `Dockerfile` не
собирался: время сборки образа и его вес после добавления слоя неизвестны.
Замер сделает `spa-skeleton`, которая этот слой и пишет.
- **Сколько живёт сборочная надстройка роутера пятой версии.** 34 пакета в
установке — число, а не суждение о том, как часто они ломаются.
+20 -7
View File
@@ -5,16 +5,20 @@
- **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует. - **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
- **Теги:** goal:web-access - **Теги:** goal:web-access
Двигает «Завершение» цели опосредованно: сам по себе каркас пользователю ничего Ни одной строки «Завершения» цели каркас не закрывает: он готовит четыре
не даёт, но без него ни один экран не соберётся. Видимая польза — приложение экранные — 1, 2, 3 и 4, — но сам по себе пользователю ничего не даёт. Видимое от
открывается и показывает, что оно живо и кто вошёл. него одно: приложение открывается и достаёт данные с живого сервера, а не отдаёт
пустую страницу.
Берётся после `spa-framework-choice`: собирать не на чем, пока фреймворк не Фреймворк выбран 2026-08-11 — Vue 3 с роутером пятой версии и сборкой Vite
выбран. ([ADR](../../docs/adr/ADR-2026-08-11-spa-on-vue.md)); правила кода приложения
лежат в [conventions/web-ui.md](../../docs/conventions/web-ui.md).
## Затрагивает ## Затрагивает
- новый каталог фронтенда: исходники, зависимости, конфигурация сборки; - новый каталог фронтенда: исходники, зависимости, конфигурация сборки;
- роут, отдающий `index.html` на неизвестный путь вне `/api/`: адреса маршрутов
обычные, а не после решётки;
- `Taskfile.yml` — шаг сборки статики и его место в `task gate` и `task image`; - `Taskfile.yml` — шаг сборки статики и его место в `task gate` и `task image`;
- `Dockerfile` — сборка статики внутри образа, чтобы выкладка не зависела от - `Dockerfile` — сборка статики внутри образа, чтобы выкладка не зависела от
машины разработчика; машины разработчика;
@@ -34,10 +38,19 @@
тест либо ручная проверка на запущенном сервере с сессией. тест либо ручная проверка на запущенном сервере с сессией.
- Шаг сборки статики входит в `task gate` и краснеет при ошибке сборки. Оракул — - Шаг сборки статики входит в `task gate` и краснеет при ошибке сборки. Оракул —
намеренно сломанный исходник роняет `task gate`. намеренно сломанный исходник роняет `task gate`.
- Обновление страницы на любом маршруте приложения открывает тот же экран, а
адрес внутри `/api/` в приложение не проваливается. Оракул — тест на два
запроса: неизвестный путь вне `/api/` отдаёт разметку, неизвестный путь внутри
`/api/` отдаёт ошибку контракта.
## Рамки ## Рамки
Экранов, кроме заглушки с именем вошедшего, не делаем — это `upload-and-status-screen` Экранов, кроме заглушки с именем вошедшего, не делаем — это `upload-and-status-screen`
и `records-list-screen`. Установку на телефон не делаем — это `installable-pwa`. и `records-list-screen`. Установку на телефон не делаем — это `installable-pwa`.
Появление шага сборки в гейте меняет требования к машине разработчика: это Готовый набор компонентов не берём: он не выбран, а способен удвоить собранный
решение уровня ADR, и оно записывается. файл.
Шаг сборки входит в гейт и в образ решением
[ADR](../../docs/adr/ADR-2026-08-11-spa-on-vue.md) 2026-08-11 — заново это
решение не принимается. Во что слой Node обходится образу по времени и по весу,
не замерялось: замер делает эта задача.