приложение собрано каркасом и вшито в бинарник
- заведён каталог web/ — Vue 3, роутер пятой версии, сборка Vite; собранное вшивается через go:embed и раздаётся корневым маршрутом: разметка на неизвестном пути вне корней сервиса, отказ контракта внутри корня - перечень корней сервиса стал единой точкой и порождает регистрацию маршрутов, а не описывает её; журнал раздачи пишет исход и длину пути, но не сам путь - шаг front зовёт Node контейнером docker — Biome, юнит-тесты Vue и сборка входят в гейт, а в Dockerfile появилась ступень приложения
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Node зовётся контейнером, а не ставится на машину разработчика
|
||||
|
||||
- **Дата:** 2026-08-15
|
||||
- **Источник:** [../../openspec/changes/archive/2026-08-15-spa-skeleton/design.md](../../openspec/changes/archive/2026-08-15-spa-skeleton/design.md),
|
||||
раздел «Node не ставится на машину, а зовётся контейнером»
|
||||
|
||||
## Решение
|
||||
|
||||
Шаг сборки приложения гоняет установщик пакетов и сборщик **внутри контейнера**,
|
||||
а не вызывает их из `PATH`:
|
||||
|
||||
> Требованием к машине разработчика становится docker, которым и так собирается
|
||||
> образ, — второго устанавливаемого окружения сверх `ffmpeg` не появляется
|
||||
> вовсе.
|
||||
|
||||
Образ сборочного окружения берётся из ступени `Dockerfile`, а не объявляется
|
||||
вторым числом в `Taskfile.yml`.
|
||||
|
||||
## Почему
|
||||
|
||||
Довод в дизайне назван прямо:
|
||||
|
||||
> Так снимается расхождение, которое иначе завелось бы молча: версия Node на
|
||||
> машине разработчика и версия в образе — два разных числа, и собранное ими
|
||||
> приложение различается ровно тогда, когда различаются они.
|
||||
|
||||
Отвергнуты два очевидных подхода, и оба с названной ценой. **Поставить Node на
|
||||
машину** — вводит второе устанавливаемое окружение и разъезжается с версией в
|
||||
образе. **Дать выбор — контейнер или локальный Node** — это второй способ делать
|
||||
одно и то же, и собранное ими различалось бы в зависимости от того, у кого что
|
||||
стоит.
|
||||
|
||||
## Почему это ADR
|
||||
|
||||
Запись проходит триггер **намеренным отказом** от очевидного подхода: поставить
|
||||
Node на машину — ровно то, что делают по умолчанию, и отказ от этого надо
|
||||
объяснить один раз, а не на каждом вопросе «почему у нас нельзя просто
|
||||
`npm run build`».
|
||||
|
||||
## Что это меняет в прежнем решении
|
||||
|
||||
[ADR-2026-08-11-spa-on-vue](ADR-2026-08-11-spa-on-vue.md) записал последствием,
|
||||
что «машина разработчика получает второе требуемое окружение сверх `ffmpeg`», и
|
||||
подразумевал под ним Node. Окружением оказался **docker**. Сам выбор фреймворка и
|
||||
наличие шага сборки это не пересматривает, поэтому статуса «заменено на» у той
|
||||
записи нет: заменена не она, а толкование одного её последствия.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` версия сборочного окружения живёт **одним** местом — ступенью
|
||||
`Dockerfile`, — и своего шага сверки ей не нужно.
|
||||
- `+` собранное в наборе проверок и собранное в образе совпадает, потому что
|
||||
совпадает окружение сборки, а не потому что «обычно совпадает».
|
||||
- `−` **набор проверок перестаёт работать без docker**, и отказ этот приходит
|
||||
кодом окружения. Тем же кодом приходит отказ реестра пакетов: сетезависимых
|
||||
шагов в наборе становится два вместо одного.
|
||||
- `−` контейнер ходит под тем же пользователем, что и вызвавший, а кэш
|
||||
установщика уводится наружу — обе частности обязательны: без них собранное
|
||||
ляжет от `root`, а зависимости будут тянуться заново каждый прогон.
|
||||
- `−` **вес и время самой ступени в образе неизвестны**: финальный образ от неё
|
||||
не растёт (ступень в рабочий слой не копируется), а время сборки решением
|
||||
владельца от 2026-08-15 не замеряется вовсе.
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-15 | [Node зовётся контейнером, а не ставится на машину разработчика](ADR-2026-08-15-node-in-container-not-on-machine.md) | |
|
||||
| 2026-08-15 | [Приложение живёт своим пространством адресов, а не общим с хранилищем](ADR-2026-08-15-app-namespace.md) | |
|
||||
| 2026-08-15 | [Страница архива задаётся ключом, а не номером](ADR-2026-08-15-cursor-paging.md) | |
|
||||
| 2026-08-15 | [Длительность и размер — снимок принятого колонками записи](ADR-2026-08-15-record-snapshot-columns.md) | |
|
||||
|
||||
+26
-3
@@ -45,6 +45,11 @@
|
||||
Здесь же обязанность, переехавшая с убранного опроса готовности: причину
|
||||
остановки владелец записи узнаёт карточкой. Задача `json-api-for-spa`
|
||||
2026-08-15;
|
||||
- [webapp](../openspec/specs/webapp/spec.md) — **приложение в браузере**: чем
|
||||
сервис его отдаёт, каким адресом оно открывается, что делает обновление
|
||||
страницы посреди него и что человек видит, открыв его. Здесь же правило
|
||||
неизвестного пути — разметка вне корней сервиса, отказ внутри, — срок хранения
|
||||
ответов и то, что раздача пишет в журнал. Задача `spa-skeleton` 2026-08-15;
|
||||
- [access](../openspec/specs/access/spec.md) — кто пришёл в сервис и пускают ли
|
||||
его дальше: вход через внешнего провайдера OIDC, чем предъявляется сессия, что
|
||||
её прекращает и какие адреса остаются открытыми. Задача `oidc-login`
|
||||
@@ -97,6 +102,8 @@
|
||||
| Репозитории | `internal/adapter/repo/pocketbase` | Записи, файлы, тексты, структура, попытки распознавания и журнал событий — коллекциями хранилища; захват — сырым запросом |
|
||||
| Шаги схемы | `internal/adapter/repo/pocketbase/migrations` | Файл на шаг, имя файла — имя шага; там же имена коллекций |
|
||||
| Панель владельца | `internal/adapter/repo/pocketbase`, `panel.go` | Панель хранилища; правила правки записи нормирует [storage](../openspec/specs/storage/spec.md), «Владелец видит записи в панели» |
|
||||
| Приложение | `web/` | Vue 3, роутер пятой версии, сборка Vite. Собранное лежит в `web/embed/dist` и вшивается в бинарник; в git его нет |
|
||||
| Раздача приложения | `internal/controller/http`, `webapp.go` | Корневой маршрут: разметка вне корней сервиса, отказ внутри, срок хранения по каталогу сборщика |
|
||||
|
||||
Цепочка рубежей — `uploaded` → `normalized` → `submitted` → `transcribed` →
|
||||
`done`; рубеж называет достигнутое, а не предстоящее, и нормирует его
|
||||
@@ -115,6 +122,11 @@
|
||||
асинхронное: запрос возвращает идентификатор операции, готовность опрашивается
|
||||
через `operation.api.cloud.yandex.net:443`, текст читается потоком.
|
||||
- **ffmpeg и ffprobe.** Внешние процессы, ищутся в `PATH`.
|
||||
- **Node и его установщик пакетов.** Нужны только сборке приложения и на машину
|
||||
не ставятся: шаг зовёт их контейнером, а образ берёт из ступени `Dockerfile`.
|
||||
Требованием к машине разработчика поэтому становится docker. Реестр пакетов —
|
||||
сетезависимый адрес набора проверок; все такие перечислены в
|
||||
[CLAUDE.md](../CLAUDE.md), «Гейт».
|
||||
|
||||
## Эксплуатация
|
||||
|
||||
@@ -187,6 +199,7 @@
|
||||
| Отображение доменной ошибки в ответ | `internal/controller/http.mapDomainError` — код, машиночитаемый код отказа и сообщение человеку; ветвь по умолчанию определена, новая ветвь заводится добавлением сюда. Отказы, рождённые слоями библиотеки (предел тела, ограничитель частоты, неизвестный путь), к той же форме приводит слой `OneErrorForm`, стоящий снаружи всех прочих |
|
||||
| Состояния отбора списка | `internal/entity.ListFilter` вместе с `WorkingStages` и `TerminalStages` — предикаты выводятся из дескриптора рубежа, а не пишутся строкой запроса |
|
||||
| Уборка имени файла отправителя | `internal/entity.SanitizeOriginalFilename` — режет по пределу и убирает управляющие знаки; зовёт её приём |
|
||||
| Адресное пространство сервиса | `internal/controller/http.ServiceMounts` — перечень корней и адресов наблюдения. Он **порождает** регистрацию наших маршрутов, а не описывает её, и из него же выводятся правило неизвестного пути и уровень журнала |
|
||||
|
||||
Единых точек, которых **нет** и которые ожидались бы: идентификаторы
|
||||
генерируются вызовом `uuid.NewString()` по месту. Время из этого перечня ушло
|
||||
@@ -201,8 +214,15 @@
|
||||
образ едет на сервер через `docker save`/`load`. Выкладку целиком запускает человек командой
|
||||
`inv pl -- transcriber` из `pet-project-server`.
|
||||
|
||||
Сборка двухступенчатая, финальный слой — alpine с `ca-certificates` и `ffmpeg`,
|
||||
процесс работает под непривилегированным пользователем `transcriber`.
|
||||
Сборка трёхступенчатая: приложение, бинарник, рабочий слой. Приложение
|
||||
собирается первым — вшивание требует готового каталога, — а в рабочий слой Node
|
||||
не попадает. Финальный слой — alpine с `ca-certificates` и `ffmpeg`, процесс
|
||||
работает под непривилегированным пользователем `transcriber`.
|
||||
|
||||
**По весу финальный образ от ступени приложения не растёт вовсе:** она отдаёт
|
||||
следующей только собранное, а сама в рабочий слой не копируется. Вшитое
|
||||
приложение прибавляет к бинарнику 86 072 байта. Время сборки образа не
|
||||
замерялось и замеряться не будет — решение владельца от 2026-08-15.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
@@ -217,7 +237,10 @@
|
||||
зависит возвращение убранного входа.
|
||||
Панель администратора при этом Authelia не закрывает: у неё свой пароль
|
||||
суперпользователя.
|
||||
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
||||
- **Приложение.** Каркас поставлен `spa-skeleton` 2026-08-15: приложение
|
||||
открывается, показывает вошедшего и вшито в бинарник. Экранов загрузки и
|
||||
списка нет — их делают `upload-and-status-screen` и `records-list-screen`.
|
||||
Решено делать SPA,
|
||||
устанавливаемое на телефон, а фреймворком взят Vue 3 с роутером пятой версии и
|
||||
сборкой Vite — 2026-08-11,
|
||||
[ADR](adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в
|
||||
|
||||
@@ -130,6 +130,9 @@
|
||||
| Скрипты оболочки | `Taskfile.yml` → шаг `shell` (`shellcheck`), он же на pre-commit |
|
||||
| Форма `Dockerfile` | `Taskfile.yml` → шаг `dockerfile` (`hadolint`), он же на pre-commit |
|
||||
| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) |
|
||||
| Форматирование и статический анализ кода приложения | `web/biome.json` → Biome, зовётся шагом `front` командой `npm run check`. Разбирает и однофайловые компоненты; правила — набор `recommended` плюс своя форма (одинарные кавычки, точка с запятой по необходимости) |
|
||||
| Типы разметки и кода приложения | `vue-tsc`, и он входит в **команду сборки**, а не стоит отдельным шагом: несобираемое приложение и непроверенные типы — один отказ |
|
||||
| Поведение экранов приложения | `Taskfile.yml` → шаг `front`, юнит-тесты Vue (`npm run test`). Без них требование «приложение показывает вошедшего» не проверял бы никто, а набор проверок оставался бы зелёным на сломанном экране |
|
||||
|
||||
### Хранилище, документы, секреты, зависимости
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ OpenSpec.
|
||||
|
||||
- Доменные поля — плоский `snake_case`.
|
||||
- Системные домены — точечная иерархия (по образцу OpenTelemetry): `http.*`,
|
||||
`ext.*`.
|
||||
`ext.*`, `webapp.*`.
|
||||
- JSON плоский: все поля на верхнем уровне, без вложенности.
|
||||
|
||||
| Когда добавляем | Поля |
|
||||
@@ -103,6 +103,8 @@ OpenSpec.
|
||||
| на задачу | `capability` (значения — по именам заведённых capability в `openspec/specs/`), `record_id`, `file_id`, `source` |
|
||||
| на запись об ошибке | `error` |
|
||||
| на вызов внешнего сервиса | `ext.service`, `ext.operation`, `ext.status_code`, `duration_ms`, `retry` |
|
||||
| на запрос, отданный приложению | `webapp.outcome` (`markup`, `asset`, `failure` — перечень закрыт), `http.path_length`. Самого пути в строке нет: его выбирает спрашивающий, и дословная запись сделала бы журнал местом, куда аноним пишет свой текст. Вместо пути в `http.route` стоит `<приложение>` |
|
||||
| на подъёме сервиса | `webapp.build` — отпечаток вшитой сборки; им «не та сборка» отличается от «той» |
|
||||
|
||||
Не заводим `service.*` и `host.*` — для одного бинарника на одном хосте это шум.
|
||||
|
||||
|
||||
@@ -10,17 +10,19 @@
|
||||
описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята
|
||||
целиком вместе со сменой решения на SPA 2026-08-10.
|
||||
|
||||
**Кода приложения ещё нет.** Правила ниже выведены из выбора и из замера на
|
||||
пробном экране, а не из написанного кода: первым их применяет и проверяет
|
||||
`spa-skeleton`. Место, где правило разойдётся с тем, что окажется удобным, —
|
||||
повод править эту запись, а не обходить её молча.
|
||||
Правила ниже применены каркасом приложения (`spa-skeleton`, 2026-08-15): до него
|
||||
они были выведены из выбора и из замера на пробном экране, а не из написанного
|
||||
кода. Место, где правило разойдётся с тем, что окажется удобным, — повод править
|
||||
эту запись, а не обходить её молча.
|
||||
|
||||
Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок
|
||||
наружу — [errors.md](errors.md).
|
||||
|
||||
**Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в
|
||||
команду сборки, а не стоит отдельным шагом. Правил линтера для кода приложения
|
||||
пока нет.
|
||||
команду сборки, а не стоит отдельным шагом. Форматирование и статический анализ
|
||||
держит **Biome**, поведение экранов — **юнит-тесты Vue**; оба шага входят в набор
|
||||
проверок наравне со сборкой. Инструменты зовутся контейнером, а не из `PATH`:
|
||||
требованием к машине разработчика остаётся docker, а не установленный Node.
|
||||
|
||||
## Что решено про само приложение
|
||||
|
||||
@@ -51,7 +53,14 @@
|
||||
делать то же самое.
|
||||
- **Собранная статика неизменяема и адресуется хешем в имени.** Имена придумывает
|
||||
Vite, руками их не задаём: от этого зависит обновление установленного
|
||||
приложения.
|
||||
приложения. Сервис на это правило опирается, но проверить его не может — имён
|
||||
он не выбирает, — поэтому дом правила здесь, а не в спеке: долгий срок
|
||||
хранения он ставит **по каталогу** сборщика, и файл, положенный туда без
|
||||
отпечатка в имени, останется в хранилище браузера навсегда.
|
||||
- **Зависимости ставятся из файла замка командой, которая его не правит.** Иначе
|
||||
набор проверок пачкает рабочее дерево, обновление зависимости приезжает в
|
||||
коммит без чьего-либо решения, а собранное в наборе проверок перестаёт
|
||||
совпадать с собранным в образе.
|
||||
- **Шаг сборки входит в `task gate` и в сборку образа.** Красная сборка статики
|
||||
роняет гейт наравне с `go build`.
|
||||
|
||||
@@ -70,7 +79,14 @@
|
||||
`/_/` у панели, — плюс `/health` и `/metrics` отдельными адресами. Приложение
|
||||
уехало из общего `/api/` решением владельца 2026-08-15: пространство
|
||||
принадлежит хранилищу, и обновление библиотеки вправе занять там имя рядом с
|
||||
нашим.
|
||||
нашим. Перечень корней сервису не описывают, а из него **порождают**
|
||||
регистрацию маршрутов: описанный порознь, он разошёлся бы с ними молча.
|
||||
- **Несовпавший ресурс разметкой не подменяется.** Путь под каталогом сборщика,
|
||||
которому не нашлось файла, отвечает `404`. Правило — вторая половина
|
||||
предыдущего: разметка прежней сборки называет ресурсы прежней сборки, и
|
||||
подменить их разметкой значит ответить `200` на то, чего нет. Браузер отвергнет
|
||||
такой ответ по типу содержимого, человек увидит пустой экран, а в кодах
|
||||
ответов сервиса не останется ничего.
|
||||
- **Экран не знает, как он открыт.** Данные экран берёт по своему адресу, а не
|
||||
получает от предыдущего: приложение открывают по ссылке и обновляют страницу
|
||||
посередине.
|
||||
@@ -107,6 +123,13 @@
|
||||
|
||||
## Что не решено
|
||||
|
||||
- **Инструмент статического анализа проверен наполовину.** Biome взят решением
|
||||
владельца 2026-08-15 и разбирает однофайловые компоненты; замены он потребует,
|
||||
если перестанет их держать. Тогда это отдельное решение, а не подстановка по
|
||||
ходу.
|
||||
- **Проверка типов держится на пятой линии TypeScript.** С седьмой `vue-tsc`
|
||||
не работает: новый компилятор не отдаёт точку входа, которую тот зовёт.
|
||||
Проверено прогоном 2026-08-15.
|
||||
- **Набор компонентов и стили.** Своя разметка или готовый набор — не решено, а
|
||||
готовый способен удвоить собранный файл
|
||||
([research/spa-framework.md](../research/spa-framework.md), «Чего разведка не
|
||||
|
||||
@@ -328,6 +328,7 @@ capability, и третий смысл развёл бы одно слово п
|
||||
| Потолок длины имени файла отправителя | 255 знаков | `entity.MaxOriginalFilenameLen` | предел длины имени в распространённых файловых системах: длиннее системный диалог выбора файла не даёт |
|
||||
| Потолок длины расширения | 32 знака | `service/transcribe.go`, `maxExtLen` | сторож от патологии, а не перечень: расширения известных форматов укладываются в пять знаков, а `x.` с четырьмястами знаками роняет заведение временного файла |
|
||||
| Потолок тем на запись | 5 | `entity.MaxTopicsPerRecord` | решение владельца: без него часовой разговор даёт два десятка тем |
|
||||
| Срок хранения ресурса приложения | 1 год | `controller/http.assetMaxAgeSeconds` | имена ресурсов несут отпечаток содержимого, поэтому ответ устареть не может; срок ставится только файлам из каталога сборщика, всё прочее браузер спрашивает заново |
|
||||
| Потолок сохранённого ответа провайдера | 256 МиБ | шаг `202608140002` | ответ многословнее расшифровки: несёт альтернативы, время каждого слова и разбор говорящих |
|
||||
| Потолок структуры реплик | 16 МиБ | там же | шестичасовой разговор даёт порядка мегабайта текста с временем |
|
||||
| Задержка перед первой проверкой операции | 10 секунд | `service/transcribe.go` | как было |
|
||||
|
||||
+3
-1
@@ -82,7 +82,9 @@ Telegram.
|
||||
|
||||
## Типовые сценарии
|
||||
|
||||
Первые два — основные, и сегодня не работает ни один: приложения нет.
|
||||
Первые два — основные, и сегодня не работает ни один. Приложение с 2026-08-15
|
||||
есть, но экранов у него пока нет: оно открывается и показывает вошедшего, а
|
||||
загрузку и список заводят `upload-and-status-screen` и `records-list-screen`.
|
||||
|
||||
1. **Семейный архив.** Человек открывает приложение на телефоне, выбирает до
|
||||
десяти записей разом — диктофонные дорожки и видео, — и закрывает его.
|
||||
|
||||
@@ -22,6 +22,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
||||
|
||||
| Дата | Запись | О чём |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-15 | [Раздача приложения: что делают за нас библиотека и сборщик](webapp-serving.md) | Раскодированный путь у маршрутизатора, второй журнал у PocketBase, нулевое время у вшитого файла, зависание установщика без сети |
|
||||
| 2026-08-13 | [Разбор TOML: какое семейство отказов несёт значения из файла](toml-decode-errors.md) | Значения только в `ParseError.Message`, врущее поле `Line`, отказ значением в BurntSushi/toml v1.5.0 |
|
||||
| 2026-08-12 | [PocketBase: умолчания, которые ломают штатный сценарий](pocketbase-defaults.md) | Потолок файла 5 МиБ, тело 32 МиБ, таймаут чтения, суффикс имени, хук правки |
|
||||
| 2026-08-11 | [gRPC-клиент SpeechKit: когда закрытие вообще может отказать](grpc-client-close.md) | Ленивое соединение и два исхода `Close` в grpc v1.74.2 |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Раздача приложения: что делают за нас библиотека и сборщик
|
||||
|
||||
Отвечает на вопросы, возникшие по ходу задачи `spa-skeleton`, — какие свойства
|
||||
раздачи приходят не из нашего кода, а из стандартной библиотеки, из PocketBase и
|
||||
из инструментов приложения. Наблюдения понадобились потому, что ревью нашло три
|
||||
места, где записанное намерение расходилось с тем, что на деле делает чужой код.
|
||||
|
||||
## Как снималось
|
||||
|
||||
Прогонами на живом бинарнике (свой конфиг с выдуманными ключами, свой каталог
|
||||
данных вне репозитория) и чтением исходников зависимостей, зафиксированных в
|
||||
`go.mod`: `github.com/pocketbase/pocketbase` версии **v0.39.10** и стандартной
|
||||
библиотеки Go. Отдельно — прогоны установщика и сборщика приложения в контейнере.
|
||||
Числа ниже сняты 2026-08-15 на этом прогоне, а не взяты из чужих записок.
|
||||
|
||||
## Что выяснилось
|
||||
|
||||
- **Маршрутизатор стандартной библиотеки сравнивает сегменты пути после
|
||||
раскодирования.** Поэтому `/%5f/` попадает туда же, куда `/_/`, а `/%68ealth`
|
||||
— туда же, куда `/health`: ответы совпадают байт в байт. Исходная форма
|
||||
остаётся в `URL.RawPath`, и решение, принимаемое **вне** сервиса по сырому пути
|
||||
— правилом обратного прокси, — такой формы не видит. Цена записана в
|
||||
[security.md](../security.md), «Периметр»: барьер перед панелью владельца
|
||||
обходится подменой одного знака.
|
||||
|
||||
- **PocketBase пишет каждый запрос в свою таблицу журнала**, а не только в вывод
|
||||
контейнера: слой `activityLogger` подключён ко всем маршрутам и кладёт путь
|
||||
целиком
|
||||
(до 3000 знаков), адрес отправителя, источник перехода и клиент. Умолчания —
|
||||
хранить пять суток, адрес записывать. Готовая раздача статики
|
||||
(`apis.Static`) первой же строкой ставит признак «успех не записывать»; своя
|
||||
раздача этого признака не наследует, и его надо ставить руками. Отсюда правило
|
||||
в [review.md](../review.md): журналов **два**, и говорить надо про оба.
|
||||
|
||||
- **Вшитая файловая система не несёт времени правки.** `embed.FS` отдаёт нулевое
|
||||
время у любого файла, поэтому отдача файла стандартной библиотекой никогда не
|
||||
отвечает подтверждением «не менялось» — всякая проверка приходит полным телом.
|
||||
Заголовок, обещающий дешёвую проверку, без метки ответа обещает то, чего код не
|
||||
делает.
|
||||
|
||||
- **Сборщик приложения чистит выходной каталог перед каждой сборкой.** Метка,
|
||||
положенная рядом с собранным ради того, чтобы каталог существовал в git,
|
||||
уезжает первым же прогоном. Живёт она только этажом выше выходного каталога.
|
||||
|
||||
- **Проверка типов однофайловых компонентов не работает с седьмой линией
|
||||
TypeScript.** `vue-tsc` версии 3.3.10 зовёт у компилятора точку входа, которой
|
||||
новый компилятор не отдаёт, и сборка падает на этапе проверки типов. Рабочая
|
||||
пара — пятая линия TypeScript.
|
||||
|
||||
- **Установщик пакетов без сети не отказывает, а виснет.** Он уходит в повторы с
|
||||
нарастающей паузой **на каждом пакете**, и набор проверок вместо кода отказа
|
||||
просто стоит. Пределы у отдельных обращений положения не спасают: их сумма и
|
||||
даёт зависание. Помогает короткое обращение-проба перед установкой.
|
||||
|
||||
- **Вес приложения в бинарнике равен весу собранного.** Замер: две сборки, с
|
||||
собранным приложением и с пустым каталогом, разница — 86 072 байта, то есть
|
||||
ровно `index.html` плюс единственный ресурс. Собранное приложение на четыре
|
||||
экрана в разведке `spa-framework` весило того же порядка.
|
||||
|
||||
## Чего эта записка не узнала
|
||||
|
||||
- **Во что ступень сборки обходится образу по времени.** Прогон до конца не
|
||||
доходит: из контейнеров этой машины нет исходящей сети при рабочем разрешении
|
||||
имён. По весу вопрос закрыт иначе — ступень в рабочий слой не копируется, и
|
||||
финальный образ от неё не растёт вовсе.
|
||||
- **Как поведёт себя раздача под настоящим потоком.** Ограничителя частоты на
|
||||
корневом маршруте нет, а профиля нагрузки у проекта нет тоже.
|
||||
- **Что делает настоящий браузер** с этими заголовками: проверено кодами ответов
|
||||
и заголовками, а не браузером.
|
||||
@@ -58,6 +58,26 @@
|
||||
- переводит доменную ошибку в свой ответ, а не отдаёт сырой текст;
|
||||
- закрывает то, что открыл, на всех ветках выхода.
|
||||
|
||||
**Раздача собранного приложения и шаг его сборки** (`controller/http/webapp.go`,
|
||||
шаг `front`):
|
||||
|
||||
- путь, принадлежащий корню сервиса, разметку не отдаёт никогда, а перечень
|
||||
корней порождает регистрацию маршрутов, а не описывает её;
|
||||
- несовпавший ресурс под каталогом сборщика отвечает `404`, а не разметкой с
|
||||
кодом `200`;
|
||||
- раздача ставит долгий неотзываемый срок хранения **только** файлу из каталога
|
||||
сборщика: отозвать его у браузера сервису нечем;
|
||||
- отсутствие сборки громкое — код ответа, страница и строка журнала; «сборки
|
||||
нет» отличается от «файла нет»;
|
||||
- вшито то, что собрано этим прогоном, а не то, что осталось от прошлого;
|
||||
- шаг следует словарю кодов: отказ сети и реестра — 3, красная сборка — 1, и он
|
||||
**отказывает, а не висит**;
|
||||
- путь, выбранный анонимом, не уходит ни меткой метрики, ни строкой журнала — и
|
||||
журналов **два**: свой, в вывод контейнера, и журнал хранилища, куда
|
||||
библиотека кладёт путь целиком вместе с адресом отправителя. Второй молчит
|
||||
только на успехе и только потому, что признак отказа от записи поставлен
|
||||
руками: готовая раздача статики ставит его сама, своя — нет.
|
||||
|
||||
**Клиент внешнего сервиса** (`adapter/recognizer/yandex`):
|
||||
|
||||
- имеет таймаут и не виснет, когда внешний сервис не отвечает;
|
||||
@@ -292,6 +312,26 @@ API и имя не откатываются обратной правкой по
|
||||
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
|
||||
оракул, и выдумывать оракул задним числом нельзя.
|
||||
|
||||
## 2026-08-15 — своя раздача статики потеряла отказ от записи успеха [пойман ревью]
|
||||
|
||||
- **Где:** `internal/controller/http/webapp.go`, регистрация корневого маршрута;
|
||||
задача `spa-skeleton`
|
||||
- **Симптом:** каждый успешный ответ разметкой и ресурсом клал в журнал
|
||||
хранилища выбранный анонимом путь вместе с его адресом и держал строку пять
|
||||
суток. При этом строка `docs/review.md`, добавленная той же задачей,
|
||||
утверждала, что путь анонима в журнал не идёт
|
||||
- **Причина:** готовая раздача статики библиотеки первой же строкой ставит
|
||||
признак «успех не записывать». Своя написана мимо неё — и не зря, подстановка
|
||||
разметки у готовой не отличает отсутствующий ресурс от неизвестного пути, — но
|
||||
признак при переписывании не перенесён.
|
||||
Журналов у сервиса два, а сделанная защита закрыла один
|
||||
- **Почему не поймали раньше:** свойство было записано **утверждением**, а
|
||||
проверялось только против журнала контейнера. Второй журнал живёт в базе, и ни
|
||||
один тест туда не смотрел
|
||||
- **Что меняем:** утверждение о журнале называет оба журнала поимённо. Оракул —
|
||||
чтение таблицы журнала после прогона: три успешных запроса не оставляют строк,
|
||||
два отказа оставляют
|
||||
|
||||
## 2026-08-15 — единая форма отказа не покрывала то, что рождается не в обработчике [пойман ревью]
|
||||
|
||||
- **Где:** `internal/controller/http/errors.go`, слой `OneErrorForm`; задача
|
||||
|
||||
+29
-3
@@ -4,9 +4,19 @@
|
||||
|
||||
**Сервис открыт наружу, но не анонимен: HTTP-порт опубликован в интернет через
|
||||
обратный прокси, а приём записи, чтение её карточки и текста и файл записи требуют входа
|
||||
через OIDC у Authelia.** Вход развёрнут задачей `oidc-login` 2026-08-12. Открыты
|
||||
без входа только проба здоровья и метрики. Находки строятся против этого —
|
||||
сегодняшнего — периметра.
|
||||
через OIDC у Authelia.** Вход развёрнут задачей `oidc-login` 2026-08-12. Без
|
||||
входа открыты проба здоровья, метрики и — с 2026-08-15, задачей `spa-skeleton` —
|
||||
**само приложение**: его разметка и её ресурсы, а вместе с ними всякий путь, не
|
||||
принадлежащий ни одному корню сервиса. Иначе не вошедший не дошёл бы до входа
|
||||
вовсе: закрытая сессией разметка отдала бы ему отказ вместо экрана. Данных
|
||||
открытость не касается — всякий адрес под корнем приложения сессии по-прежнему
|
||||
требует. Находки строятся против этого — сегодняшнего — периметра.
|
||||
|
||||
**Состав того, что отдаётся анонимно, задаёт содержимое собранного приложения**,
|
||||
а каталог его лежит в `.gitignore` и не судится ничем: всё, что окажется там у
|
||||
собирающего, уезжает в бинарник и раздаётся. Под каталогом ресурсов оно ещё и
|
||||
отдаётся с годовым сроком хранения и пометкой «неизменяемо» — отозвать выданное
|
||||
браузеру сервису нечем.
|
||||
|
||||
Целевой периметр добавляет к нему отдельный вход для программ по личным токенам
|
||||
и два уровня доступа — пользователь видит свои записи, владелец сервиса ещё и
|
||||
@@ -37,6 +47,17 @@ Telegram — связи чата с учётной записью сервис
|
||||
администраторов. Задачи в беклоге у этого нет — работа принадлежит выкладке, а
|
||||
она вне модели («Что вне модели», строка про контур).
|
||||
|
||||
**И этот барьер обходится подменой одного знака.** Маршрутизатор сравнивает
|
||||
сегменты пути **после** раскодирования, поэтому `/%5f/` попадает в ту же группу,
|
||||
что и `/_/`, а правило прокси написано на литерал и такой формы не видит.
|
||||
Проверено прогоном 2026-08-15 ревью задачи `spa-skeleton`: обе формы отвечают
|
||||
байт в байт, и весь клиент панели грузится анониму. Вход в приложение при этом
|
||||
не обходится — `/%61pp/me` отвечает `401`. Дефект старше задачи, которая его
|
||||
нашла, и **сегодня не закрыт**: лечение — приведение пути к канонической форме на
|
||||
стороне сервиса, и глухая проверка тут не годится, потому что сломает скачивание
|
||||
файлов с пробелами и не-латиницей в имени. Половину пути проверить нечем: правило
|
||||
прокси живёт в `pet-project-server`, вне этого репозитория.
|
||||
|
||||
**Четвёртый сдвиг — секрет клиента поселился в базе.** Задача `oidc-login`
|
||||
2026-08-12 кладёт адреса провайдера, идентификатор клиента и его секрет в
|
||||
настройки коллекции пользователей, приводя их к конфигу при каждом подъёме
|
||||
@@ -186,6 +207,11 @@ Storage, оттуда его читает SpeechKit. Третий путь —
|
||||
её нет ни у пробы, ни у сборщика. Наружу их закрывает правило обратного
|
||||
прокси — работа выкладки, и сервис на неё не полагается: содержимого записей
|
||||
эти адреса не несут.
|
||||
- **Приложение** — его разметка и ресурсы открыты без сессии, и ограничителя
|
||||
частоты на них нет: правило заведено под корень приложения, а раздача стоит
|
||||
вне его. Содержимого записей ни разметка, ни ресурсы не несут: они одинаковы
|
||||
для всех и собраны до всякого запроса. По ответу нельзя узнать, вошёл ли
|
||||
кто-то, — вошедшему и не вошедшему отдаётся одно и то же.
|
||||
|
||||
Владение записью в модели данных появилось 2026-08-14: у задачи и у её файла
|
||||
есть владелец. Знание идентификатора задачи правом её читать больше не является
|
||||
|
||||
Reference in New Issue
Block a user