diff --git a/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md b/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md index 1674771..494905b 100644 --- a/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md +++ b/docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md @@ -3,6 +3,7 @@ - **Дата:** 2026-08-11 - **Источник:** [../research/pocketbase.md](../research/pocketbase.md) — записка разведки `pocketbase-admin-fit` +- **Статус:** заменено на [ADR-2026-08-22-storage-without-pocketbase](ADR-2026-08-22-storage-without-pocketbase.md) ## Решение diff --git a/docs/adr/ADR-2026-08-11-queue-as-pocketbase-collection.md b/docs/adr/ADR-2026-08-11-queue-as-pocketbase-collection.md index 96b23b0..99a0045 100644 --- a/docs/adr/ADR-2026-08-11-queue-as-pocketbase-collection.md +++ b/docs/adr/ADR-2026-08-11-queue-as-pocketbase-collection.md @@ -3,6 +3,7 @@ - **Дата:** 2026-08-11 - **Источник:** [../research/job-queue.md](../research/job-queue.md) — записка разведки `job-queue-choice` +- **Статус:** заменено на [ADR-2026-08-22-storage-without-pocketbase](ADR-2026-08-22-storage-without-pocketbase.md) ## Решение diff --git a/docs/adr/ADR-2026-08-12-protected-file-behind-session.md b/docs/adr/ADR-2026-08-12-protected-file-behind-session.md index f9dfa32..b23bb7c 100644 --- a/docs/adr/ADR-2026-08-12-protected-file-behind-session.md +++ b/docs/adr/ADR-2026-08-12-protected-file-behind-session.md @@ -5,6 +5,7 @@ раздел «Что изменило ревью кода», плюс отчёт триажа [../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), пункт 3 +- **Статус:** заменено на [ADR-2026-08-22-storage-without-pocketbase](ADR-2026-08-22-storage-without-pocketbase.md) ## Решение diff --git a/docs/adr/ADR-2026-08-22-storage-without-pocketbase.md b/docs/adr/ADR-2026-08-22-storage-without-pocketbase.md new file mode 100644 index 0000000..46fbfe8 --- /dev/null +++ b/docs/adr/ADR-2026-08-22-storage-without-pocketbase.md @@ -0,0 +1,79 @@ +# Хранилищем становится SQLite с каталогом файлов, а PocketBase уходит целиком + +- **Дата:** 2026-08-22 +- **Источник:** [../research/storage-without-pocketbase.md](../research/storage-without-pocketbase.md) — + записка разведки о выборе хранилища + +## Решение + +PocketBase уходит из проекта целиком: состояние записей и метаданные переезжают +в SQLite, с которым сервис работает напрямую через `modernc.org/sqlite`, файлы +записей — в свой каталог со своей раскладкой, маршруты и слои — на `net/http`, +шаги схемы — на свой раннер. Панель администратора теряется и **не заменяется +ничем**: пока идёт стройка, остановленную запись возвращает в работу запрос к +базе. + +Запись заменяет три: +[ADR-2026-08-11-pocketbase-storage-with-admin-panel](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md), +[ADR-2026-08-11-queue-as-pocketbase-collection](ADR-2026-08-11-queue-as-pocketbase-collection.md) и +[ADR-2026-08-12-protected-file-behind-session](ADR-2026-08-12-protected-file-behind-session.md). + +**Что из заменённых решений подтверждается, а не отменяется.** Очередь остаётся +своей таблицей, захват — одним запросом с `RETURNING`, готовую библиотеку +очереди по-прежнему не берём: замер, которым это решено, снят на +`modernc.org/sqlite` — том самом драйвере, который остаётся и после ухода. +Отменяется у той записи одно слово: таблица перестаёт быть коллекцией. +Приложение остаётся в своём корне `/app/` +([ADR-2026-08-15-app-namespace](ADR-2026-08-15-app-namespace.md)). Файл записи +остаётся закрытым — но проверкой владельца в своём обработчике, а не защищённым +полем коллекции и коротким токеном. + +## Почему + +Разведка мерила не «хранилище против хранилища», а то, что библиотека держит в +этом коде. Цитата из источника: + +> Разрез «хранилище против хранилища» вопроса не покрывает: библиотека держит +> шесть ролей сразу, и только две из них про хранение. + +Довод, на котором стоял перевод, отпал сам. Источник, раздел о трёх доводах: + +> Вход делает само приложение с 2026-08-22 […]: пришедшего называет заголовок +> прокси, а учётную запись заводит наш `EnsureUser`. Пользователи в коллекции +> есть **потому, что их пишет наш код**, а не провайдер библиотеки. Довод, +> которым отвергнут отвергнутый вариант, перестал быть верным. + +Отвергнут вариант «уйти в два шага», оставив панель жить в промежутке, и +отвергнут решением владельца: панель на стройке заменяется запросом к базе, а +вторая порция работы стоит дороже, чем то, что она сберегает. + +Обстоятельство, которое назначило момент: + +> на сервере данных нет и сервис остановлен, поэтому смена стоит только кода. +> Дешевле она не станет никогда — каталог `internal/controller/http` прирастает +> кодом на чужих типах с каждой задачей. + +## Последствия + +- `+` периметр сервиса становится только нашим. Панель `/_/` исчезает вместе с + дефектом `/%5f/` из [../security.md](../security.md), а пространство + хранилища `/api/` — вместе с необходимостью держать его открытым ради файлов. +- `+` пропадает секрет, которого не было до перевода, — пароль суперпользователя + панели. +- `+` файлы ложатся своей раскладкой, и загрузка частями, узнавание по хеш-сумме, + удаление записи и вторая копия рядом становятся обычной работой с файлами. +- `+` из сборки уходят шесть модулей, достижимых только через библиотеку: + `imaging`, `mailyak`, `jwt`, `fexpr`, `cobra`, драйвер MySQL. + `modernc.org/sqlite` остаётся, и сборка по-прежнему обходится без CGO. +- `−` владелец сервиса остаётся без панели. Правку записи, возврат остановленной + в работу и просмотр очереди до появления экранов делает запрос к базе руками. + Задачи `audiorecord-actions` и `play-recording-in-app` этим становятся не + улучшением, а заменой утраченного инструмента. +- `−` шаги схемы, отдачу файла, ограничитель частоты и настройку базы пишем и + сопровождаем сами. Единственный писатель у `modernc.org/sqlite` — наша забота + с этого дня. +- `−` раскладка каталога данных меняется необратимо. Цена сегодня нулевая: + стройка, на сервере пусто; после первой боевой записи она перестаёт быть + нулевой. +- `−` 1497 строк контроллера и 3075 строк его проверок написаны на + `*core.RequestEvent` и переписываются целиком. diff --git a/docs/adr/README.md b/docs/adr/README.md index ec1bc47..7f7fa74 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -35,6 +35,7 @@ | Дата | Запись | Статус | | --- | --- | --- | +| 2026-08-22 | [Хранилищем становится SQLite с каталогом файлов, а PocketBase уходит целиком](ADR-2026-08-22-storage-without-pocketbase.md) | | | 2026-08-22 | [Пришедшего называет заголовок доверенного прокси, а не собственный вход OIDC](ADR-2026-08-22-login-by-trusted-header.md) | | | 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) | | @@ -49,7 +50,7 @@ | 2026-08-14 | [Учётная запись с записями не удаляется, и это осознанный тупик](ADR-2026-08-14-account-with-records-is-not-deleted.md) | | | 2026-08-13 | [Намерение объявляется признаком, а не выводится из ключа доступа](ADR-2026-08-13-telegram-intent-declared-not-inferred.md) | устарело: вход убран [ADR-2026-08-15-telegram-intake-removed-temporarily](ADR-2026-08-15-telegram-intake-removed-temporarily.md) | | 2026-08-13 | [Недоступность Telegram подъёму сервиса не мешает](ADR-2026-08-13-telegram-outage-does-not-block-startup.md) | устарело: вход убран [ADR-2026-08-15-telegram-intake-removed-temporarily](ADR-2026-08-15-telegram-intake-removed-temporarily.md) | -| 2026-08-12 | [Файл записи закрыт защищённым полем и отдаётся вошедшему по токену файла](ADR-2026-08-12-protected-file-behind-session.md) | | +| 2026-08-12 | [Файл записи закрыт защищённым полем и отдаётся вошедшему по токену файла](ADR-2026-08-12-protected-file-behind-session.md) | заменено на [ADR-2026-08-22-storage-without-pocketbase](ADR-2026-08-22-storage-without-pocketbase.md) | | 2026-08-12 | [Сессия живёт семь суток и не продлевает саму себя](ADR-2026-08-12-session-without-refresh.md) | заменено на [ADR-2026-08-22-login-by-trusted-header](ADR-2026-08-22-login-by-trusted-header.md) | | 2026-08-12 | [Кого пускать в сервис, решает правило провайдера, а не сервис](ADR-2026-08-12-access-delegated-to-provider.md) | | | 2026-08-12 | [Код провайдера меняется на сессию вызовом собственного адреса хранилища внутри процесса](ADR-2026-08-12-oidc-exchange-via-own-route.md) | заменено на [ADR-2026-08-22-login-by-trusted-header](ADR-2026-08-22-login-by-trusted-header.md) | @@ -61,8 +62,8 @@ | 2026-08-11 | [Отказ, который решено не проверять, объявляется поимённо](ADR-2026-08-11-errcheck-check-blank.md) | | | 2026-08-11 | [Наружу расширение выходит только приведённым к перечню](ADR-2026-08-11-known-format-label.md) | | | 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-pocketbase-storage-with-admin-panel.md) | | +| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | заменено на [ADR-2026-08-22-storage-without-pocketbase](ADR-2026-08-22-storage-without-pocketbase.md) | +| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | заменено на [ADR-2026-08-22-storage-without-pocketbase](ADR-2026-08-22-storage-without-pocketbase.md) | | 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | | Решения, принятые до заведения канона 2026-08-10, источника в архиве изменений diff --git a/docs/research/README.md b/docs/research/README.md index bf6f3cd..5d6c780 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -22,6 +22,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт | Дата | Запись | О чём | | --- | --- | --- | +| 2026-08-22 | [Хранилище: PocketBase против голого SQLite с каталогом файлов](storage-without-pocketbase.md) | Шесть ролей библиотеки в этом коде, отпавший довод перевода, объём кода на её типах, шесть модулей только через неё | | 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 МиБ, таймаут чтения, суффикс имени, хук правки | diff --git a/docs/research/storage-without-pocketbase.md b/docs/research/storage-without-pocketbase.md new file mode 100644 index 0000000..9ede870 --- /dev/null +++ b/docs/research/storage-without-pocketbase.md @@ -0,0 +1,127 @@ +# Хранилище: PocketBase против голого SQLite с каталогом файлов + +Отвечает на вопрос владельца от 2026-08-22: не окажется ли голый SQLite с +каталогом аудиозаписей гибче встроенной PocketBase. Записи в каталоге задач у +вопроса нет — он поднят по ходу работы, и разведка идёт от него, а не от +постановки. + +Мерилось **дерево репозитория**, а не внешний сервис: вопрос о том, что +библиотека уже держит в этом коде и чем за это плачено. Замеров на живом потоке +нет. + +## Как снималось + +Дата — 2026-08-22, коммит `a8fb479`, дерево чисто. Всё считано в самом +репозитории, боевые данные не участвовали. + +- **Пакеты в сборке:** `go list -deps ./... | wc -l` → **584**; из них с + `pocketbase` в пути — `go list -deps ./... | grep -c pocketbase` → **40**. +- **Кто тянет тяжёлый модуль:** `go mod why -m <модуль>` по семи модулям. +- **Объём кода:** `find <каталог> -name '*.go' [! -name '*_test.go'] | xargs wc -l`. +- **Протечка библиотеки за адаптер:** `grep -l "pocketbase/" internal/controller/http/*.go`. +- **Узлы для варианта с монтированием** — чтением кэша модулей: + `router.Router.BuildMux()` отдаёт `http.Handler` + (`tools/router/router.go:61`), а `apis.Serve` присваивает `e.Server.Handler` + (`apis/serve.go:223`). Прототипа на этих двух узлах не собирал — проверено + только их существование. + +## PocketBase здесь — фреймворк приложения, а не хранилище + +Разрез «хранилище против хранилища» вопроса не покрывает: библиотека держит +шесть ролей сразу, и только две из них про хранение. + +| Роль | Где | Чем заменяется | +| --- | --- | --- | +| SQLite без CGO, пул записи одним соединением | `pbrepo.New` | `modernc.org/sqlite` напрямую, свои WAL, `busy_timeout` и единственный писатель | +| Схема и шаги миграций | `internal/adapter/repo/pocketbase/migrations`, 1039 строк | свой раннер либо `goose`, который тут уже был | +| Файлы записей на диске и отдача `/api/files/…` по токену | `file_repo.go`, `FileTokenPath` | каталог и свой обработчик отдачи | +| Маршрутизатор, цепочка слоёв с приоритетами, ограничитель частоты | `internal/controller/http` целиком | `net/http` и счётчик по ключу | +| Учётная запись значением `e.Auth` | `identity.go` | строка своей таблицы | +| Панель `/_/` | покупалась ради неё | ничем | + +**Библиотека вышла за адаптер.** Из пяти не-тестовых файлов +`internal/controller/http` её импортируют **все пять**, из шести тестовых — +**все шесть**: 1497 строк кода контроллера и 3075 строк его проверок написаны +на `*core.RequestEvent`. Адаптер хранилища — ещё 1787 строк без шагов схемы. + +## Три довода прежнего решения: один умер 2026-08-22 + +[ADR-2026-08-11-pocketbase-storage-with-admin-panel](../adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) +покупал библиотеку за три вещи разом, и отвергал половинчатые пути тем, что +порознь ни одна перевода не оправдывает. + +1. **Правка записей в панели** — работает. Единственная роль, которой сегодня + нет замены. +2. **Файлы видны в панели, и копия накрывает их вместе с базой** — работает; тот + же ADR оговаривает, что копии сервер делает своими средствами. +3. **Вход через её провайдер OIDC** — **отпал**. Цитата, которой ADR отверг + вариант «взять библиотеку только хранилищем»: + + > Панель показывает свою коллекцию пользователей и ничего больше. Отсюда + > следствие для целевого входа: **пользователи Authelia в панели не появятся, + > если вход делает само приложение**. + + Вход делает само приложение с 2026-08-22 + ([ADR-2026-08-22-login-by-trusted-header](../adr/ADR-2026-08-22-login-by-trusted-header.md)): + пришедшего называет заголовок прокси, а учётную запись заводит наш + `EnsureUser`. Пользователи в коллекции есть **потому, что их пишет наш код**, + а не провайдер библиотеки. Довод, которым отвергнут отвергнутый вариант, + перестал быть верным — и вместе с ним отпало основание держать вход в + библиотеке. + +## Что оплачено и не работает + +- **Захват записи идёт сырым запросом мимо записей коллекции** + (`record_repo.go`, `FindAndAcquire`): `app.DB().NewQuery` с `UPDATE … RETURNING`. + На самом горячем месте абстракция не помогает, а её ограничения действуют — + хуки коллекции не срабатывают, время изменения проставляет наш запрос, времена + сравниваются строками побайтово. +- **Налог соседства двух периметров в одном процессе и на одном порту.** + Появился секрет, которого не было, — пароль суперпользователя. Панель + опубликована в интернет, и её барьер обходится подменой знака: `/%5f/` + попадает в ту же группу, что `/_/`, а правило прокси написано на литерал + ([../security.md](../security.md)); дефект не закрыт. Узнавание нельзя + повесить на всю поверхность хранилища, иначе узнанный перепишет себе + `provider_login` и заберёт чужой архив — область слоя сужена, и причина стоит + абзацем в `identity.go`. +- **Пространство `/api/` нельзя закрыть на прокси**, потому что за файлами + ходит туда браузер пользователя. Своя отдача файла снимает это ограничение: + закрыть можно всё пространство хранилища разом. + +## Чем платит уход + +- **Панель теряется целиком.** Правка записи, возврат остановленной в работу, + просмотр очереди фильтром, прослушивание файла — всё это сегодня живёт только + там. +- **Пишем сами** шаги схемы, отдачу файла, ограничитель частоты и настройку + базы. Последняя — не формальность: у `modernc.org/sqlite` запись идёт + единственным соединением, и библиотека держит это за нас двумя пулами. +- **Раскладка каталога данных** названа необратимой в `../../CLAUDE.md`. + На стройке цена нулевая: на сервере пусто, переносить нечего. + +## Что уходит из сборки, а что остаётся + +`go mod why -m` показал, что шесть модулей достижимы **только** через +PocketBase: `disintegration/imaging` (через `tools/filesystem`), +`domodwyer/mailyak/v3` и `golang-jwt/jwt/v5` (через `core`), +`ganigeorgiev/fexpr` (через `apis`), `spf13/cobra` (набор команд), +`go-sql-driver/mysql` (через `pocketbase/dbx`). `modernc.org/sqlite` остаётся: +он нужен и без библиотеки, и требование обходиться без CGO с ним сохраняется. + +## Варианты и что выбрано + +| Вариант | Что делает | Чем платит | +| --- | --- | --- | +| Оставить как есть | ноль работы | долг растёт с каждой задачей на `RequestEvent` | +| **Уйти целиком** | SQLite напрямую, каталог файлов, свои шаги схемы и маршруты | панель теряется сразу, работа одной порцией | +| Уйти в два шага | сначала снять с периметра HTTP, панель оставить; потом с хранилища | панель жива в промежутке, работа та же, но двумя порциями | +| Локализовать протечку | библиотека не выходит за `internal/adapter/repo` | решение откладывается, панель остаётся | + +**Решением владельца от 2026-08-22 взят уход целиком, и панель не заменяется +ничем**: пока стройка не кончилась, остановленную запись возвращают в работу +запросом к базе. Решение записано в +[ADR-2026-08-22-storage-without-pocketbase](../adr/ADR-2026-08-22-storage-without-pocketbase.md). + +Обстоятельство, которое решило дело: на сервере данных нет и сервис остановлен, +поэтому смена стоит только кода. Дешевле она не станет никогда — каталог +`internal/controller/http` прирастает кодом на чужих типах с каждой задачей. diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 0b096c2..099a1b1 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -43,6 +43,7 @@ ## Очередь +- [✨ Уйти с PocketBase на SQLite со своим каталогом файлов](items/storage-without-pocketbase.md) — Библиотека держит шесть ролей и вышла за адаптер: её типы стоят во всех пяти файлах контроллера и во всех шести его проверках, панель опубликована в интернет вместе с необойдённым /%5f/, а пространство /api/ нельзя закрыть на прокси, потому что за файлами туда ходит браузер пользователя. - [🧹 Поднимать сервис для локальной работы одной командой](items/dev-run-task.md) — Локальная проверка требует ручной чистки каталога данных и остановки процесса, а владельца панели заводят по одноразовой ссылке из журнала. - [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит. - [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем. diff --git a/tasks/items/storage-without-pocketbase.md b/tasks/items/storage-without-pocketbase.md new file mode 100644 index 0000000..fb8b0e6 --- /dev/null +++ b/tasks/items/storage-without-pocketbase.md @@ -0,0 +1,47 @@ +# ✨ Уйти с PocketBase на SQLite со своим каталогом файлов + +- **Тип:** feature +- **Категория:** Очередь — Всё остальное строится поверх хранилища: экраны пишутся на типах контроллера, а dev-run-task заводит владельца панели, которой не станет +- **Зачем:** Библиотека держит шесть ролей и вышла за адаптер: её типы стоят во всех пяти файлах контроллера и во всех шести его проверках, панель опубликована в интернет вместе с необойдённым /%5f/, а пространство /api/ нельзя закрыть на прокси, потому что за файлами туда ходит браузер пользователя. + +Сервис работает с SQLite напрямую, файлы записей лежат в своём каталоге и +отдаются своим обработчиком, маршруты и слои живут на `net/http`, схему двигают +свои шаги. Панель администратора исчезает вместе с адресами `/_/` и `/api/`. + +Решение — [ADR-2026-08-22-storage-without-pocketbase](../../docs/adr/ADR-2026-08-22-storage-without-pocketbase.md), +разведка — [storage-without-pocketbase](../../docs/research/storage-without-pocketbase.md). + +## Затрагивает + +- каталог `internal/adapter/repo/pocketbase` целиком и его шаги схемы; +- каталог `internal/controller/http` целиком вместе с проверками; +- подъём сервера и цепочку слоёв в `cmd/transcriber/main.go`; +- раскладку каталога данных: файл базы и каталог файлов записей — **необратимое**; +- публичный контракт HTTP: пространство `/api/` и адрес `/_/` исчезают, корень + `/app/` остаётся; +- отдачу файла записи браузеру: короткий токен файла и защищённое поле коллекции; +- панель администратора как инструмент владельца сервиса; +- ключ конфига `[storage] data_dir` и перечень зависимостей `go.mod`; +- модель угроз: адрес `/_/`, дефект `/%5f/` и правило прокси на пространство + хранилища. + +## Критерии приёмки + +- Библиотеки нет в сборке. **Оракул:** `go list -deps ./... | grep -c pocketbase` + даёт `0`, а `go mod tidy` не возвращает её модулей в `go.mod`. +- Чужой файл недостижим по прямой ссылке. **Оракул:** тест контроллера — запрос + к файлу чужой записи и к несуществующей отвечает одинаково. +- Захват записи остаётся неделимым. **Оракул:** тест с параллельными воркерами + под `-race` — запись достаётся ровно одному, второй получает отказ по значению + признака захвата. +- Пространства хранилища не существует. **Оракул:** тест маршрутов — `/api/…`, + `/_/` и `/%5f/` отвечают тем же, чем всякий неизвестный путь. +- Схема накатывается на пустом каталоге до старта воркеров. **Оракул:** запуск на + чистом каталоге данных — ни одного отказа в журнале до первой строки о готовности. + +## Рамки + +Данные не переносятся: стройка, на сервере пусто. Панель не заменяется ничем — +остановленную запись возвращает в работу запрос к базе, пока этого не сделают +экраны владельца. Очередь остаётся своей таблицей с захватом одним запросом +`RETURNING`. Боевой каталог данных не трогать.