Files
jellybit/openspec/changes/archive/2026-06-30-web-ui-design-port/design.md
T
avandClaude Opus 4.8 4e2593ac31 Перенос дизайна в server-rendered веб-UI (web-ui)
Новая capability web-ui: презентационный перенос готового дизайна
(семантический HTML + единый jellybit.css, тёмная тема по настройке ОС)
в html/template без шага сборки.

- Встроенная (go:embed) отдача статики под /static с Cache-Control и
  cache-busting (?v=<hash> по содержимому css/js).
- Шрифты IBM Plex self-hosted (@font-face, cyrillic+latin), без CDN.
- Наколеночный менеджер зависимостей: вендор (htmx + шрифты) не хранится
  в репо (gitignore), идемпотентно добывается `task assets` по
  web/assets.manifest с проверкой sha256; task build/run зависят от assets.
- Общие партиалы: шапка, бейдж статуса (карта всех 14 состояний),
  виджет «файл источника → раскладка» (общий для review и download).
- Страницы: список с фильтром/поиском, ревью, новая страница просмотра
  загрузки (/download/{id}). deleted скрыт по умолчанию.
- Превью раскладки берётся из единой логики internal/layout
  (buildFileRows), без дублирования правил имён в шаблонах.
- Убраны meta-refresh и инлайн-стили; copyHash на vanilla с fallback
  на execCommand и честной индикацией (целевой деплой — HTTP LAN).

Вне scope (отдельный change): живые обновления прогресса и раздел
раздачи, клиентский режим ручной раскладки файл→серия (с Alpine.js).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:59:19 +03:00

156 lines
12 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.
## Context
Веб-UI — тонкий транспорт над ядром (приём в `ingest`, команды в `worker`,
чтение в `store`). Рендер server-rendered через `html/template`, ассеты
встраиваются `go:embed` (`web/web.go`). Сейчас встроены только шаблоны;
статика (CSS/JS/шрифты) не отдаётся, «живость» сделана грубым
`<meta http-equiv="refresh" content="5">`, оформление — инлайн-`<style>` с
хардкодом цветов.
Готов дизайн (`tmp/design/`): семантический HTML + единый `jellybit.css`
(токены, тёмная тема по ОС), спроектированный под этот стек — без сборки,
npm, React. Превью целевых путей раскладки уже считается на сервере через
`internal/layout.BuildLinks` (поле `reviewView.Preview`) — это источник истины
имён, дублировать его нельзя (инвариант: превью = реальная раскладка).
Стек зафиксирован с заказчиком: htmx (через манифест с checksum, без Node),
шрифты self-hosted, статика через `go:embed`. Реактивных фреймворков нет:
клиентский режим ручной раскладки файл→серия отложен (операция редкая), вместе
с ним отложен и Alpine.js — его добавим, когда виджет понадобится. Живые
обновления прогресса — отдельный change (`web-live-updates`), здесь meta-refresh
просто убирается.
## Goals / Non-Goals
**Goals:**
- Перенести дизайн `tmp/design/` в прод-шаблоны без шага сборки.
- Встроить и отдавать статику (CSS, htmx, Alpine, шрифты) из одного бинаря.
- Единая дизайн-система: один CSS, токены, тёмная тема по ОС, бейджи статусов.
- Общие партиалы (шапка, бейдж, виджет «файл→раскладка») вместо дублей.
- Добавить страницу `download` (просмотр одной загрузки).
- Превью путей на ревью — показ из единого серверного расчёта `internal/layout`.
**Non-Goals:**
- Живые обновления прогресса/SSE (отдельный change).
- Клиентский режим ручной раскладки файл→серия (S·E, нумерация, live-превью) и
Alpine.js под него — отложены до реальной необходимости (операция редкая).
- Изменение доменного поведения (приём, распознавание, петля ревью, команды).
- Авторизация веб-UI (остаётся вне scope, доверенная LAN).
- npm/бандлер/CSS-постпроцессинг.
## Decisions
### D1. Статика через `go:embed` + `http.FileServer` под `/static/`
`web/web.go` встраивает `static/*` рядом с `templates/*`. Роут
`r.Handle("/static/*", http.StripPrefix("/static/", fileServer))` с
`Cache-Control` (длинный max-age для вендорных файлов и шрифтов; для CSS —
умеренный, либо cache-busting через `?v=<hash>`, посчитанный при старте).
*Альтернативы:* отдавать с диска — отвергнуто (ломает однобинарность);
CDN для htmx/Alpine — отвергнуто (внешняя сеть, инвариант self-host).
### D2. Шрифты self-hosted через `@font-face`
IBM Plex Sans/Mono кладём в `web/static/fonts/`, подключаем `@font-face` в
`jellybit.css`. Из шаблонов убираем `<link>` на Google Fonts.
*Почему:* umbar — домашний сервер, не зависим от внешней сети, не утекает
referrer. Handoff сам это рекомендует.
### D3. Зависимости фронта — наколеночный менеджер: gitignore + идемпотентный фетч
Вендорные ассеты (htmx + шрифты IBM Plex) **не храним в репозитории**
`web/static/vendor/` в gitignore. Источник истины — манифест `web/assets.manifest`
(строки `<путь под web/static/> <url> <sha256>`). `task assets` идемпотентен:
для каждой записи — если файл есть и sha256 совпал, берём с диска; иначе качаем и
сверяем (несовпадение валит таргет). Node/бандлер не вводим. Alpine.js не вводим
— реактивных виджетов в этом change нет (см. Non-Goals).
**Граница вендора.** В vendor только фетчабельные третьесторонние пакеты (htmx,
woff2-шрифты). Авторские `jellybit.css` (с вшитым `@font-face` на `../vendor/
fonts/`) и `app.js` — наш исходник, коммитятся как обычно (качать неоткуда).
**Сборка.** `go:embed templates static` встраивает и vendor, поэтому файлы должны
быть на диске к моменту `go build`. `task build`/`task run` зависят от `task
assets`. Бинарь остаётся самодостаточным (всё встроено), репозиторий — без
вендора. Размен: канонична сборка через Task; голый `go build` без `task assets`
соберёт бинарь с неполной статикой (css есть, шрифтов/htmx нет) — это принято.
*Альтернативы:* (1) коммитить результат фетча — отвергнуто заказчиком (не хотим
вендор в репо). (2) npm + бандлер (Parcel/esbuild) через Docker — отвергнуто
сейчас: бандлить нечего (htmx — готовый `<script>`, своего JS ~ноль), целый
рантайм ради пары файлов. Дорастёт фронт до своих компонентов — отдельный change
и esbuild (проще Parcel; есть Go-биндинги, можно без Node).
### D4. Партиалы через `template.ParseFS` нескольких файлов
Выносим общие куски в `web/templates/partials/`: `header.html`,
`status_badge.html` (карта `state→class/подпись`), `layout_widget.html`
(виджет «файл источника → раскладка», общий для review и download). Бейдж
рендерится по `state` — карту держим в одном партиале (`{{define}}`), не
дублируем по страницам. Текущий `add`-FuncMap сохраняем; при нужде добавляем
маленькие чистые функции-хелперы.
*Почему:* handoff (п. 7) прямо просит вынести шапку, бейдж и виджет в общие
партиалы — сейчас они продублированы.
### D5. `download.html` — новая страница `/download/{id}`
Новый GET-хендлер + view-структура: распознавание, файлы→раскладка (тот же
партиал), раздача, история/таймлайн. Действия — по состоянию (как в index),
формы переиспользуют уже существующие `/ui/downloads/{id}/*` эндпоинты.
### D6. Ручная раскладка файл→серия — отложена
Клиентский режим ручного редактирования (S·E по файлам, нумерация,
live-превью) в этот change не входит — операция редкая, не оправдывает
реактивный виджет и Alpine.js сейчас. На ревью остаётся показ файлов и
серверного превью (read-only) + существующие действия через формы/htmx
(уточнить/перераспознать/выбрать кандидата/применить/позже/отклонить).
*Когда понадобится* — заведём отдельный change: тогда же выберем подход
(клиентская валидация + серверный dry-run превью переиспользует
`layout.BuildLinks`, чтобы не дублировать правила имён в JS) и добавим Alpine.
### D7. Убрать meta-refresh и инлайн-стили
`<meta http-equiv="refresh">` и `<style>` удаляются. До change живых
обновлений список статичен (обновление — перезагрузкой). Это осознанный
временный шаг, чтобы не смешивать презентацию с поведенческим изменением.
## Risks / Trade-offs
- **Устаревание/дрейф htmx** → версия и sha256 в манифесте; обновление
осознанное (правка манифеста + `task assets` с проверкой хеша).
- **URL-rot / офлайн-сборка** (vendor не в репо) → первая сборка требует сети,
дальше файлы кэшируются на диске (идемпотентно). Митигация: пиннинг версий +
два рабочих зеркала (unpkg/jsdelivr), URL правится в манифесте. Если понадобится
жёсткая офлайн-воспроизводимость — внутреннее зеркало или возврат к коммиту.
- **Регресс действий при переносе шаблонов** (поле без `name`, не та форма) →
опираемся на разметку `<!-- action: … -->` в прототипах и существующие
хендлеры; проверяем каждое действие по состоянию; ревью кода до archive.
- **Временная потеря авто-обновления** (D7) → приемлемо, закрывается
следующим change; список обновляется перезагрузкой.
- **Рассинхрон карты статусов** между бейджем и доменом → карта в одном
партиале + спека требует полноты покрытия всех состояний.
## Migration Plan
1. Манифест + идемпотентный `task assets` (фетч htmx и шрифтов в gitignored
`web/static/vendor/` с проверкой sha256); `task build`/`run` зависят от
`assets`; добавить авторские `web/static/{css,js}`, расширить `go:embed`.
2. Роут `/static/*` + `Cache-Control`.
3. Партиалы (header, status_badge, layout_widget); переключить index/review.
4. Добавить `download.html` + хендлер `/download/{id}`.
5. Убрать meta-refresh и инлайн-стили.
6. `task lint` + `task test`; ручная проверка действий по состояниям.
Откат — презентационный, без миграций БД и схемы: revert коммита
восстанавливает прежние шаблоны.
## Open Questions
- Cache-busting CSS: `?v=<hash>` при старте или фиксированный `Cache-Control`
с коротким max-age. Решим при реализации D1 (склонность — хеш при старте).