From b34f23a23034195a6ae0ff324c20182e664bb26d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 09:41:59 +0300 Subject: [PATCH] spec: define containerized application delivery #10 --- .../.openspec.yaml | 2 + .../design.md | 247 ++++++++++++++++++ .../proposal.md | 46 ++++ .../specs/application-delivery/spec.md | 105 ++++++++ .../tasks.md | 219 ++++++++++++++++ 5 files changed, 619 insertions(+) create mode 100644 openspec/changes/containerized-application-delivery/.openspec.yaml create mode 100644 openspec/changes/containerized-application-delivery/design.md create mode 100644 openspec/changes/containerized-application-delivery/proposal.md create mode 100644 openspec/changes/containerized-application-delivery/specs/application-delivery/spec.md create mode 100644 openspec/changes/containerized-application-delivery/tasks.md diff --git a/openspec/changes/containerized-application-delivery/.openspec.yaml b/openspec/changes/containerized-application-delivery/.openspec.yaml new file mode 100644 index 0000000..0c96227 --- /dev/null +++ b/openspec/changes/containerized-application-delivery/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd +created: 2026-09-16 diff --git a/openspec/changes/containerized-application-delivery/design.md b/openspec/changes/containerized-application-delivery/design.md new file mode 100644 index 0000000..dbd2850 --- /dev/null +++ b/openspec/changes/containerized-application-delivery/design.md @@ -0,0 +1,247 @@ +## Context + +Frontend и backend выпускаются из независимых Git-репозиториев. Frontend — +Vite SPA, backend — NestJS API с embedded SQLite, Prisma migrations, native +Node dependencies и runtime `.proto` contracts. Браузерная сессия использует +Bearer access token и Secure httpOnly refresh-cookie с path `/api/v1/auth`. + +На production-сервере уже работает Caddy. Он должен знать только домен и один +loopback port WealthLedger; TLS и server-wide routing остаются вне приложения. +Краткий простой обновляемого компонента допустим. Текущие кэши, rate limiter и +connection generation допускают только один активный backend instance. + +Root workspace связывает независимые checkout, но его CI по действующему +контракту выполняет только commitlint. Поэтому orchestration принадлежит root, +а сборка и публикация каждого image — репозиторию компонента. + +## Goals / Non-Goals + +**Goals:** + +- запускать весь application runtime локально через Docker с Linux, общей + container network, единым browser origin и быстрым frontend/backend watch; +- поставлять отдельные immutable frontend и backend images; +- публиковать наружу только frontend port, через который доступны SPA и API; +- хранить SQLite и backend secrets вне images и сохранять их при recreation; +- применять Prisma migrations до готовности новой версии backend; +- обновлять frontend или backend отдельно с явной парой установленных версий; +- дать оператору воспроизводимые backup, rollback и restore boundaries. + +**Non-Goals:** + +- zero-downtime, blue-green, несколько backend replicas и высокая доступность; +- PostgreSQL, Kubernetes, container orchestration вне одного сервера; +- управление Caddy, DNS, TLS, host firewall или provisioning сервера; +- изменение HTTP API, session semantics или пользовательского интерфейса; +- общий release номер для независимых репозиториев. + +## Decisions + +Уточнение Change Owner от 2026-09-16: обязательная настройка digests отменена. +Production принимает `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags. +Публикация сохраняет запрет перезаписи release tags; `latest` не используется. + +### 1. Docker Compose владеет application topology + +Root workspace получает production Compose и development override. Production +Compose использует опубликованные image references; development override +заменяет их local development targets, подключает исходники и отдельное local +data storage. Новые custom orchestration scripts не создаются: lifecycle +выражается штатными Compose commands и документированной последовательностью. + +Production содержит два long-running services: + +- `frontend`: nginx со статической SPA и proxy `/api/` в `backend:3000`; +- `backend`: один NestJS process с доступом к persistent SQLite storage. + +Одноразовый migration service использует тот же backend image и data storage, +завершается до старта новой версии backend. Наружу публикуется только nginx на +настраиваемый loopback host port. Caddy проксирует весь домен на этот port и не +маршрутизирует `/api/` отдельно. + +Development сохраняет те же component и network boundaries, но frontend target +запускает Vite с proxy `/api/` в `backend:3000`, а backend target запускает Nest +watch. Browser видит только frontend port. Source mounts не включают host +`node_modules`; Linux dependencies принадлежат images/volumes. Изменение +manifest или lockfile требует rebuild соответствующего development image. + +Отдельный nginx gateway в development не добавляется: он создал бы третий +постоянный service, но всё равно использовал бы отличающуюся proxy-конфигурацию +для Vite HMR. Production image и его nginx routing проверяются отдельным +production Compose smoke scenario. + +### 2. Frontend и backend имеют собственные multi-stage images + +Frontend image собирает SPA точной project Node version и переносит только +build output и nginx configuration в version-pinned nginx runtime. Production +SPA использует same-origin API base. nginx поддерживает SPA history fallback, +передаёт `/api/` без переписывания prefix и повторно разрешает Docker DNS имя +backend после recreation. + +Backend image собирается под Linux точной project Node version. Runtime содержит +compiled application, production dependencies, generated Prisma client, +migrations и T-Bank `.proto` contracts по пути, который использует production +process. Prisma CLI становится явным runtime tool backend image; test, lint и +compiler tooling в runtime не переносится. Process работает не от root и пишет +только в выделенный data path. + +Оба images получают OCI metadata с repository revision и version. Base images +и package installs закрепляются воспроизводимыми версиями; новая platform или +architecture не обещается без отдельной проверки. + +### 3. Gitea Container Registry хранит независимые release images + +Frontend и backend workflows публикуют собственный image после push защищённого +release tag `vX.Y.Z`. Publish job повторяет канонический repository gate, +проверяет совпадение tag с package version и только затем отправляет semver tags в Gitea Container Registry. `latest` не используется. + +Server deployment фиксирует полные frontend/backend image references с +semver tags; оператор выбирает опубликованную версию каждого компонента. CI использует отдельный secret с +package write permission, сервер — отдельный credential только для pull. Ни один +credential не хранится в repository или image. Возможность встроенного Gitea +workflow token публиковать packages не предполагается без проверки фактической +server version. + +Root не получает image workflow: каждый repository самостоятельно доказывает и +публикует свой artifact, а Compose только фиксирует установленную пару image references. + +### 4. Same-origin routing сохраняет действующую auth boundary + +Production frontend nginx и development Vite направляют исходный `/api/...` в +backend без изменения path. Frontend client использует relative API base. +Backend получает `FRONTEND_ORIGIN`, равный browser-visible origin; production +работает с `NODE_ENV=production`, поэтому refresh-cookie остаётся Secure. + +nginx передаёт host, forwarding headers и request ID context, не журналирует +Authorization, Cookie или request body. Backend port доступен только Compose +network. Server host публикует frontend на loopback, чтобы Caddy оставался +единственным внешним listener. + +### 5. Readiness опирается на существующий runtime contract + +Migration service должен завершиться успешно до запуска backend. Backend +healthcheck подтверждает завершённый HTTP-ответ от существующего auth route; +статус `401` без cookie является допустимым доказательством готового HTTP/auth +boundary. Новый публичный health endpoint не добавляется. Frontend healthcheck +получает статический root и зависит от healthy backend при полном старте. + +Compose `--wait` используется как bounded readiness gate. Он не является +автоматическим rollback и не обещает отсутствие краткого `502` во время +отдельного backend update или недоступность всей точки входа во время frontend +recreation. + +### 6. SQLite остаётся single-server storage в явно ограниченной роли + +Production использует один backend process и один persistent host data +directory. Это расширяет прежнее локальное решение ADR 001: docs получают новый +ADR, который разрешает SQLite только для текущего single-server deployment и +фиксирует условия пересмотра — несколько replicas, иной availability target, +рост write contention или потребность в отдельном database service. + +Перед migration backend останавливается и создаётся согласованная копия всего +data directory. Остановка исключает конкурентные writes и позволяет сохранить +database вместе с возможными WAL companion files без нового backup service. +Encryption key хранится отдельно от data backup; восстановление требует оба +артефакта. + +Local development использует другой named volume/data directory и другие +secrets. Production data никогда не монтируется в development environment. + +### 7. Обновления меняют только выбранный service + +Frontend update меняет только frontend image reference, загружает image и +recreates frontend без зависимостей. Backend и data остаются запущенными, но +единая внешняя точка входа кратко недоступна во время recreation. + +Backend update сначала сохраняет backup при остановленном backend, затем меняет +backend image reference. Migration service и backend используют один reference; backend +стартует только после успешной migration и становится доступен после healthcheck. +Frontend не пересоздаётся. nginx повторно разрешает service DNS после смены +backend container address. + +Release compatibility остаётся обязанностью API evolution: новая сторона +ставится первой только когда прежняя сторона совместима с ней. Compose-файл +фиксирует пару image references, но не вводит второй version negotiation mechanism. + +## UI/UX Applicability and Package + +**Applicability and scope:** `N/A` — change меняет development, packaging и +server operations, но не экраны, состояния, interactions или responsive +поведение WealthLedger. Новый визуальный пакет не требуется. + +## Delivery Map + +| Area | Status | Architectural outcome or N/A reason | +| --- | --- | --- | +| frontend | `affected` | Multi-stage development/production image, same-origin client configuration, Vite API proxy, nginx SPA/API routing и image publication workflow. | +| backend | `affected` | Multi-stage development/production image, runtime Prisma migration assets, container readiness и image publication workflow. | +| specs | `affected` | Новая capability `application-delivery` и утверждаемый SDD package. | +| docs | `affected` | Deployment/runbook, обновлённая container topology и ADR ограниченного single-server SQLite deployment. | + +Root workspace дополнительно владеет Compose orchestration и кратким entrypoint +README как cross-area coordination; это не переносит component build ownership +из frontend/backend и не добавляет root CI помимо commitlint. + +## Solution Assessment + +| Вариант | Fit и зрелость | Security / operations | Ограничения и migration cost | Решение | +| --- | --- | --- | --- | --- | +| Docker Compose + два component images | Нативно покрывает один server, service DNS, healthchecks, volumes и targeted recreation; широко поддерживается | Малый operating surface; registry credentials и host Docker требуют обычной защиты | Краткий простой, один host, нет automatic rollback | Выбран как минимальное полное решение | +| Один объединённый application image | Простая установка одного artifact | Один lifecycle и один process boundary проще оператору | Связывает независимые релизы, пересобирает неизменённый component, усложняет watch и нарушает repository ownership | Отклонён | +| Kubernetes или Swarm | Зрелая orchestration для replicas и rolling updates | Добавляет control plane, политики и постоянную эксплуатацию | Не нужен одному server и не решает текущие SQLite/multi-instance ограничения | Отклонён до появления HA требований | +| Caddy маршрутизирует отдельно frontend и backend | Использует существующий server proxy | Убирает internal gateway layer | Server должен знать внутреннюю topology, появляются два application upstream и deployment перестаёт быть self-contained | Отклонён по выбранной внешней границе | +| Отдельный постоянный gateway service внутри Compose | Стабильный port при frontend recreation | Может уменьшить внешний простой frontend update | Третий runtime service и конфигурация без требования zero-downtime | Отклонён; пересмотреть при SLO без краткого простоя | +| SQLite на persistent directory | Уже реализован, зрел для single-process embedded workload, не требует database service | Минимальная сеть и credentials; backup требует остановки | Один writer/process topology, нет HA; новая ADR boundary обязательна | Выбран в ограниченной single-server роли | +| PostgreSQL service | Зрел для concurrency, replicas и независимого DB lifecycle | Требует отдельные credentials, backup, upgrades и monitoring | Миграция Prisma provider и существующих данных вне scope; текущая нагрузка не требует | Отклонён до появления multi-instance/availability требования | +| Собственная packaging/orchestration layer | Может точно повторить частные команды | Создаёт собственный security и maintenance surface | Дублирует Compose, registry и standard image lifecycle | Отклонён | + +Gitea Container Registry и Actions используются как уже доступные mature +platform capabilities. Предварительное условие Apply — runner с Docker build/push +capability и package permissions; при отсутствии этого условия Design не +заменяется самописным registry uploader, а publish task остаётся blocked. + +## Risks / Trade-offs + +- Frontend recreation кратко отключает и SPA, и API ingress; это принято + текущим non-goal zero-downtime. +- Backend update может кратко возвращать `502`; healthcheck ограничивает окно, + но не устраняет его. +- Старые browser tabs могут запрашивать chunks прежнего frontend. nginx задаёт + revalidation для HTML и immutable cache для hashed assets, но прежние assets + после recreation не сохраняются. Пользователю может потребоваться reload; + автоматическое восстановление старых вкладок остаётся вне текущего scope. +- SQLite на server расширяет прежний local-only baseline. Новый ADR делает это + осознанным ограничением, а не молчаливым изменением production guarantee. +- Воспроизводимость version tags зависит от запрета их перезаписи: workflow + отклоняет существующий tag. Переназначать опубликованные tags нельзя. +- Development работает через Vite, production — через nginx. Production Compose + smoke проверяет различающийся web-server boundary до поставки. +- Полная runtime image воспроизводимость зависит от base-image availability; + digests и сохранённые release images уменьшают, но не устраняют registry risk. + +## Migration Plan + +1. Утвердить SDD package и новый ADR о границе SQLite на одном сервере. +2. Подготовить component images и проверить их отдельно с каноническими gates. +3. Добавить root Compose topology и development override; подтвердить watch, + same-origin API, отсутствие внешнего backend port и изоляцию local data. +4. Добавить release-tag publication в component repositories и получить version tags + проверенных images из Gitea Registry. +5. На server создать restricted environment/secrets и data directories, + сохранить исходную базу и encryption key, затем выполнить первый migration + service и production smoke через loopback port. +6. Настроить Caddy на единственный loopback port и проверить login, refresh, + чтение существующих данных и один внешний financial request. +7. Зафиксировать установленную пару image references и удалить прежний runtime + только после успешной проверки нового. + +### Rollback Contract + +| Field | Decision | +| --- | --- | +| Status | `required` | +| Reason | Change вводит server persistence, migrations, secrets и новую delivery boundary. | +| Trigger | Migration failure, unhealthy component, невозможность login/refresh, нечитаемое encrypted connection, повреждение данных или критическая регрессия smoke scenario. | +| Actions | Не направлять Caddy на первый deployment либо вернуть предыдущий component image reference. Старую версию backend запускать только после проверки совместимости с текущей schema. Если schema несовместима, остановить приложение и отдельно предложить восстановление согласованного backup; не выполнять restore автоматически. | +| Data safety | Backup создаётся при остановленном backend из всего data directory; encryption key сохраняется отдельно. Данные, созданные после backup, считаются более новыми и не заменяются без явного решения оператора о потере этого интервала. | +| Verification | Проверить component health, schema migration status, login/refresh, чтение существующих user/card data, decrypt/use сохранённого T-Bank connection и отсутствие повторной migration ошибки; затем записать фактически установленную пару image references. | diff --git a/openspec/changes/containerized-application-delivery/proposal.md b/openspec/changes/containerized-application-delivery/proposal.md new file mode 100644 index 0000000..065185f --- /dev/null +++ b/openspec/changes/containerized-application-delivery/proposal.md @@ -0,0 +1,46 @@ +## Why + +WealthLedger сейчас запускается как набор локальных процессов, а поставка на +сервер не определена. Разработка не воспроизводит границы runtime-среды, а +оператор не имеет согласованного способа установить, обновить, сохранить или +восстановить приложение. + +Нужен единый поддерживаемый путь запуска для локальной разработки и одного +сервера, чтобы поставка каждой версии была воспроизводимой и не требовала +ручного пересоздания окружения. + +## What Changes + +Будет определён поддерживаемый способ запускать WealthLedger локально и на +одном сервере. Внешняя серверная маршрутизация передаёт весь трафик +приложения на одну точку входа WealthLedger; приложение само обслуживает +интерфейс и API. + +Оператор сможет независимо обновлять интерфейс или API согласованными +версиями поставки. Пользовательские данные, сессии и секреты сохраняются +между штатными перезапусками и обновлениями. Для обновления и восстановления +будут определены необходимые безопасные действия и их границы. + +Не входят непрерывное обновление без краткого простоя, несколько экземпляров +API, высокая доступность, смена хранилища данных, изменение внешнего reverse +proxy и управление серверной инфраструктурой вне WealthLedger. + +## Capabilities + +### New Capabilities + +- `application-delivery`: поддерживаемый локальный и серверный запуск, + обновление компонентов и сохранность состояния WealthLedger. + +### Modified Capabilities + +Нет. + +## Impact + +Пользователь получает одинаково работающую среду разработки и поставленное +приложение. Оператор получает воспроизводимую установку, независимое +обновление частей приложения и понятные границы восстановления данных. +Изменение затронет frontend, backend, specs, docs и общую orchestration +workspace; детали решений и delivery будут определены в последующих +утверждаемых артефактах. diff --git a/openspec/changes/containerized-application-delivery/specs/application-delivery/spec.md b/openspec/changes/containerized-application-delivery/specs/application-delivery/spec.md new file mode 100644 index 0000000..179a418 --- /dev/null +++ b/openspec/changes/containerized-application-delivery/specs/application-delivery/spec.md @@ -0,0 +1,105 @@ +## Purpose + +Определяет поддерживаемые эксплуатационные свойства локального и серверного +запуска WealthLedger, включая поставку компонентов и сохранность состояния. + +## ADDED Requirements + +### Requirement: Локальная среда воспроизводит границы поставленного приложения + +Поддерживаемый локальный запуск SHALL предоставлять один browser origin для +интерфейса и API WealthLedger. Разработка интерфейса MUST сохранять быстрый +цикл обратной связи при изменении исходного кода, а разработка API MUST +перезапускать затронутый runtime без ручной пересборки всей среды. Локальные +данные MUST быть изолированы от server data. + +#### Scenario: Разработчик открывает локальное приложение + +- **WHEN** поддерживаемая локальная среда успешно запущена +- **THEN** браузер получает интерфейс и обращается к API через тот же origin, + не зная внутреннего адреса API + +#### Scenario: Разработчик меняет интерфейс + +- **WHEN** разработчик изменяет исходный код интерфейса в запущенной локальной + среде +- **THEN** браузер получает изменение без ручной полной пересборки среды + +### Requirement: Серверное приложение имеет одну внешнюю точку входа + +Поддерживаемый server deployment SHALL принимать HTTP-трафик WealthLedger на +одной внешней точке входа. Через неё MUST быть доступны интерфейс и API с +сохранением их действующих путей и session semantics. API и хранилище данных +MUST NOT требовать отдельного внешнего сетевого доступа. + +#### Scenario: Внешний reverse proxy направляет запрос приложению + +- **WHEN** server reverse proxy передаёт запрос на единственную точку входа + WealthLedger +- **THEN** пользователь получает интерфейс, а запросы к действующему API + обрабатываются приложением без отдельного маршрута reverse proxy к API + +### Requirement: Поставка допускает независимое обновление компонентов + +Система SHALL предоставлять versioned delivery artifacts для интерфейса и API +как независимых компонентов. Оператор MUST иметь возможность установить +выбранную версию только одного компонента, сохранив другой компонент и +постоянное пользовательское состояние работающими. Поддерживаемая поставка +MUST явно фиксировать выбранные версии обоих компонентов. + +#### Scenario: Обновляется только интерфейс + +- **WHEN** оператор устанавливает новую версию интерфейса при неизменной + совместимой версии API +- **THEN** API и его постоянное состояние не пересоздаются, а приложение + начинает отдавать новую версию интерфейса + +#### Scenario: Обновляется только API + +- **WHEN** оператор устанавливает новую версию API при неизменной совместимой + версии интерфейса +- **THEN** интерфейс не пересоздаётся, а приложение начинает обрабатывать + запросы новой версией API после её готовности + +### Requirement: Постоянное состояние и секреты переживают штатный lifecycle + +Пользовательские данные, session state и ключи, необходимые для чтения +зашифрованных подключений, MUST сохраняться при штатных перезапусках и +обновлениях компонентов. Секреты MUST задаваться отдельно от versioned delivery +artifacts и MUST NOT попадать в интерфейс, журналы или публичный HTTP-ответ. + +#### Scenario: Приложение штатно перезапускается + +- **WHEN** оператор штатно перезапускает приложение без восстановления данных +- **THEN** после запуска доступны прежние пользовательские данные и действующие + session semantics, а сохранённые зашифрованные подключения остаются читаемыми + тем же ключом + +### Requirement: Обновление API сохраняет целостность данных + +Перед тем как новая версия API начинает принимать запросы, необходимые +изменения схемы постоянных данных MUST быть применены успешно. При ошибке +изменения данных новая версия API MUST NOT объявляться готовой. Краткий простой +обновляемого компонента допустим; непрерывность обслуживания и несколько +одновременных экземпляров API не являются обещанием этой capability. + +#### Scenario: Подготовка данных новой версии не прошла + +- **WHEN** обязательное изменение постоянных данных завершается ошибкой +- **THEN** новая версия API не начинает обслуживать запросы, а оператор получает + диагностируемый неуспешный результат обновления + +### Requirement: Восстановление не выдаёт откат приложения за откат данных + +Поддерживаемая процедура обновления и восстановления MUST различать возврат +версии компонентов и восстановление пользовательских данных. Она MUST требовать +согласованную резервную копию перед изменением постоянных данных и MUST NOT +заменять более новые пользовательские данные прежней копией как побочный эффект +возврата версии приложения. + +#### Scenario: Оператор возвращает предыдущую версию API + +- **WHEN** оператор возвращает прежнюю версию API после неуспешного обновления +- **THEN** процедура явно проверяет совместимость этой версии с текущими + данными и не выполняет восстановление старой копии данных без отдельного + решения оператора diff --git a/openspec/changes/containerized-application-delivery/tasks.md b/openspec/changes/containerized-application-delivery/tasks.md new file mode 100644 index 0000000..f7c6cc6 --- /dev/null +++ b/openspec/changes/containerized-application-delivery/tasks.md @@ -0,0 +1,219 @@ +# Контейнерная разработка и поставка WealthLedger + +Основание — утверждённые [Proposal](proposal.md), +[Delta Specs](specs/application-delivery/spec.md) и [Design revision 1](design.md). +UI/UX package неприменим: пользовательские экраны и interactions не меняются. + +Checkbox закрывается только после выполнения действия и его Verification. +Перед Node-командами активируется `.nvmrc` соответствующего независимого +репозитория. Git-команды выполняются из репозитория-владельца; production data, +secrets и опубликованные release tags не используются как test fixtures. + +Задачи выполняются по порядку разделов. Frontend 2.1–2.2 и backend 3.1–3.2 +могут идти параллельно после 1.2. Workspace orchestration начинается после +готовности обоих component images. Реальный server rollout выполняется только +после локального production smoke и документированного backup. + +## 1. SDD и внешние prerequisites + +- [x] 1.1 [specs] Проверить утверждённый SDD package закреплённым OpenSpec CLI и сохранить canonical change структурно валидным до Apply. + - **Covers:** `N/A` — обязательный gate canonical SDD authority; он не добавляет runtime behavior, но разрешает реализацию утверждённых требований и Design. + - **Verification:** с версией из `openspec/.cli-version` выполнить `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate containerized-application-delivery --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, затем `npm run verify`. + - **Result (2026-09-16):** по явному уточнению владельца использован установленный OpenSpec 1.13.0 вместо 1.10.0. Все четыре проверки прошли; 25 specs passed, 0 failed. Repository-wide pin не изменён. + +- [ ] 1.2 [workspace] Подтвердить prerequisites выбранного Solution Assessment: Docker Compose на development/server hosts, Docker-capable Gitea runner, Container Registry и раздельные package write/read credentials. + - **Covers:** `N/A` — техническое предусловие утверждённой Compose/Registry delivery; оно не вводит самостоятельное поведение приложения и блокирует custom fallback при отсутствии platform capability. + - **Verification:** зафиксировать поддерживаемые Docker/Compose и Gitea versions; `docker version`, `docker compose version`, успешные authenticated registry login для CI publisher и server pull-only identity без вывода credentials. + - **Partial result (2026-09-16):** локально Docker Desktop 4.91.0, Engine 29.8.0 (Linux arm64), Compose 5.5.1; Gitea API сообщает 1.27.3. Server Docker/Compose и Docker-capable runner пока не подтверждены. MCP identity не имеет доступа к Actions secrets (`user should be the owner of the repo`); раздельные publisher/pull-only login не проверены. Publication и server rollout остаются внешними gates; независимая локальная реализация продолжается. + +## 2. Frontend image и ingress + +- [x] 2.1 [frontend] Добавить development и production image targets: Dockerized Vite watch с same-origin `/api/` proxy и nginx runtime со SPA fallback, API proxy, runtime DNS re-resolution, cache policy и non-root-compatible filesystem. + - **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`. + - **Verification:** собрать оба targets; development container через container network отдаёт HMR и сохраняет `/api/v1/...`; production container отдаёт SPA/deep link, проксирует API без rewrite, не раскрывает backend port и после backend recreation подключается к новому container address. + +- [ ] 2.2 [frontend] Добавить release-tag workflow публикации frontend image с проверкой версии, canonical frontend gate, OCI revision metadata и immutable semver tags без `latest`. + - **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`. + - **Verification:** workflow syntax принимается Gitea; controlled candidate run после `npm run verify` публикует image, registry возвращает digest, clean pull по digest запускает тот же static artifact; credential отсутствует в logs и image history. + +- [x] 2.3 [frontend] Выполнить frontend repository completion gate после packaging и routing changes. + - **Covers:** `N/A` — repository-owned проверка реализации Design и отсутствие regression в существующем frontend; отдельного observable requirement не создаёт. + - **Verification:** `npm ci`, `npm run verify`, `git diff --check`; вручную просмотреть новые Docker/nginx/workflow files и отсутствие secrets. + +### Результаты frontend (2026-09-16) + +- 2.1: оба targets собраны на Linux arm64; Vite HMR WebSocket connected, + `/api/v1/probe?check=path` сохранён. nginx read-only + tmpfs отдаёт deep link + с `no-cache`, proxy сохраняет path/query/host/proto/request ID. Backend без + host port пересоздан с другим IP (.2 → .5); nginx восстановил proxy без restart. +- 2.2: release-tag workflow реализован с `ubuntu-latest` (label подтверждён + владельцем). Реальный runner Docker access, registry write/read credentials, + Gitea workflow acceptance и controlled publication пока не проверены. +- 2.3: `npm ci`, `npm run verify`, `git diff --check` прошли. Docker context + ограничен allowlist; `.env`, credentials и host dependencies не копируются. + Новые Docker/nginx/workflow files просмотрены; publication secrets идут + только в отдельный Docker login, не в image build arguments. + +## 3. Backend image, migration и readiness + +- [x] 3.1 [backend] Добавить development и production image targets с Linux native dependencies, compiled application, generated Prisma client, runtime Prisma CLI/migrations, T-Bank proto assets, non-root process и выделенным data path. + - **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`. + - **Verification:** development target выполняет Nest watch; production target стартует из чистого image, находит proto contracts, принимает config только из environment, пишет только в data path; одноразовый `prisma migrate deploy` проходит на fresh и representative existing SQLite, а приложение не стартует после контролируемой migration failure. + +- [ ] 3.2 [backend] Добавить release-tag workflow публикации backend image с проверкой версии, полными backend gates, OCI revision metadata и immutable semver tags без `latest`. + - **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`. + - **Verification:** workflow syntax принимается Gitea; controlled candidate run выполняет `npm run verify` и `npm run test:e2e`, публикует image и получает digest; clean pull по digest выполняет migration и production start, credential отсутствует в logs и image history. + +- [x] 3.3 [backend] Выполнить backend repository completion gates и проверить container readiness без нового публичного endpoint. + - **Covers:** `N/A` — repository-owned проверка packaging, existing auth boundary и SQLite integration из Design; самостоятельного нового API behavior нет. + - **Verification:** `npm ci`, `npm run verify`, `npm run test:e2e`, `git diff --check`; healthcheck принимает завершённый HTTP-ответ existing auth route, а stopped/uninitialized process остаётся unhealthy. + +## 4. Workspace Compose orchestration + +- [x] 4.1 [workspace] Добавить production Compose topology с version-tagged frontend/backend references, migration service, healthchecks, persistent host data directory, internal-only backend и единственным configurable frontend loopback port. + - **Covers:** `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`. + - **Verification:** `docker compose config` с non-secret fixture environment; rendered config содержит один host port, один backend replica, одинаковый backend image reference у migration/runtime и отдельный data mount; missing required image/config/secret values дают blocking configuration/start failure. + +- [x] 4.2 [workspace] Добавить development Compose override с local image targets, source watch/mounts, container-owned dependencies, отдельным local data volume и одним frontend browser origin. + - **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`. + - **Verification:** с чистыми local volumes запустить Compose; frontend edit появляется через HMR, backend edit перезапускает Nest, `/api/v1/...` идёт через frontend origin, host `node_modules` не используется, restart сохраняет local data, production data path отсутствует в rendered config. + +- [x] 4.3 [workspace] Обновить root tracking allowlist и README entrypoints без custom scripts или нового root CI, затем проверить orchestration configuration. + - **Covers:** `N/A` — root только связывает независимые component repositories и объясняет стандартные Compose entrypoints согласно Design; runtime behavior реализуется component images и Compose tasks 4.1–4.2. + - **Verification:** `docker compose config` для production и development variants, `git diff --check`, просмотр `git status --short` и staged paths подтверждают только явно разрешённые root files; root workflow остаётся только commitlint. + +## 5. Эксплуатационная документация + +- [x] 5.1 [docs] Добавить ADR об ограниченном single-server SQLite deployment и обновить container architecture фактической Docker/Caddy topology, secret и persistence boundaries. + - **Covers:** `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`. + - **Verification:** ADR явно связывает решение с прежним local-only baseline, одним backend instance и triggers пересмотра; architecture diagram показывает только frontend port наружу, internal backend и persistent SQLite; ссылки и термины согласованы. + +- [x] 5.2 [docs] Описать development, first deployment, independent component update, stopped-backend backup, compatibility check, rollback и explicit restore runbooks. + - **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`. + - **Verification:** dry-run runbook на disposable data проходит без неописанного шага; frontend-only и backend-only paths не пересоздают другой component; restore требует отдельного решения и сообщает о потере данных после backup. + +- [ ] 5.3 [docs] Выполнить docs repository completion gate. + - **Covers:** `N/A` — repository-owned проверка durable as-built documentation и ссылок без нового runtime behavior. + - **Verification:** `npm ci`, `npm run verify`, `git diff --check`; проверить Mermaid rendering, взаимные ADR/architecture/runbook links и отсутствие credentials или host-private values. + +## 6. Сквозная проверка поставки + +- [x] 6.1 [workspace] Проверить development Compose на чистом checkout и production Compose на локально собранных release-equivalent images. + - **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`. + - **Verification:** development подтверждает HMR/watch и same-origin login; production smoke подтверждает SPA deep link, registration/login/refresh, API proxy, отсутствие host backend port и clean startup через migration service. + +- [x] 6.2 [workspace] На disposable representative SQLite проверить persistence, frontend-only update, backend-only update, migration failure и rollback boundaries. + - **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`. + - **Verification:** restart сохраняет user/session/card data и decryptable test connection; frontend update не recreates backend/data, backend update не recreates frontend; failed migration не делает backend healthy; возврат совместимого image сохраняет current data, а restore отдельной backup copy выполняется только в изолированном scenario с явной проверкой потерянного интервала. + +- [ ] 6.3 [workspace] Проверить release images из Gitea по выбранным version tags перед первым server rollout и сохранить точную установленную пару версий. + - **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`. + - **Verification:** pull-only server identity получает оба versioned images; `docker image inspect` подтверждает expected revisions; production smoke проходит на этих images с disposable config/data, после чего оператор записывает выбранные image references без secrets. + +## Apply verification — 2026-09-16 + +### Backend (3.1–3.3) + +- Development и production targets собраны на Linux arm64 / Node 24.21.0. + Node/nginx base images закреплены digest; OS packages — точными версиями. + Чистый production runtime обнаружил неверный прежний fallback proto path: + Nest копирует assets в `dist/src`; loader теперь использует `__dirname`. + Nest watch использует `ps`; `procps` добавлен только в development target. +- `npm ci`, `npm run verify` — PASS (35 suites). `npm audit` при переносе + существующего Prisma 7.10.0 в runtime dependencies — 0 vulnerabilities. +- `npm run test:e2e` на macOS дважды завершился native Node SIGABRT + (`RemoveEnvironmentCleanupHook`, `env != nullptr`). Тот же штатный gate + выполнен в Linux development image: **8 suites / 98 tests PASS**. Это + Linux delivery proof; macOS native cleanup limitation не скрывается. +- `npm run test:container -- wealthledger-backend:production` — runnable + image regression: fresh/existing migration, runtime без source/.env/compiler, + non-root + read-only filesystem, существующий auth readiness, user/session + persistence. Он же вызывается publish workflow после clean pull. +- Controlled broken SQLite migration возвращает non-zero; backend остаётся + остановленным. После возвращения исправного data path migration идемпотентна. +- 3.2 остаётся открытой: YAML и shell syntax проверены локально, но Gitea + acceptance, authenticated publication и pull по registry digest не выполнены. + +### Compose, docs и lifecycle (4–6) + +- Production/development `docker compose config` — PASS. Проверены один + loopback host port, backend scale 1, общий backend image/data mount у + migration/runtime, readiness dependency и отсутствие production bind mount + в development services. Пустая production environment блокирует config. +- `docker compose up -d --wait` — PASS для development и production на + локально собранных release-equivalent images. Production smoke использовал + временный override local image references; canonical config остаётся + digest-pinned. Реальный Gitea digest pull этим не подменяется. +- Development: source edit вызвал HMR event и изменённый HTTP module, backend + source edit — новый Nest process и готовый API. Временные изменения + восстановлены. Same-origin registration/login/refresh и data restart PASS. +- Clean checkout proof: отдельный HEAD export + task diff, без host .env и + node_modules, новый Compose project/volume; startup и HMR/watch PASS. +- Browser proof (bundled Playwright/Chrome): clean development и production + registration/login/reload-refresh PASS; все API requests same-origin, + httpOnly/path сохранены, production cookie Secure. Caddy/TLS вне локальной проверки. +- Production: SPA deep link, registration/login/refresh, неизменённый API path, + отсутствие backend host port, non-root read-only runtime — PASS. +- Representative disposable SQLite содержит user/session/card и connection, + зашифрованный настоящим TbankCrypto на вымышленном token. Restart, migrations + и independent recreation сохраняют данные и расшифровку без внешнего T-Bank. +- Отдельные local fixture image IDs проверили frontend-only/backend-only + update без recreation другого компонента. Возврат прежнего совместимого + backend image сохраняет запись после backup. Это compatibility fixture, + не обещание совместимости произвольных будущих релизов. +- Stopped-backend backup и isolated restore с отдельным project/port/data path + — PASS: login, карта до backup и decryptable connection доступны; запись + после backup отсутствует в восстановленной копии. Текущая база не заменялась. +- Docs: ADR 015 расширяет ADR 001; topology и runbook взаимно связаны. + `npm ci`, `npm run verify` — PASS (44 Markdown files), `git diff --check` + — PASS. Mermaid source семантически просмотрен, но renderer в доступном + окружении не найден; **5.3 оставлена открытой до проверки rendering**. +- Root Boy Scout/allowlist review: только Compose, два безопасных env examples, + README и .gitignore; новых root scripts/dependencies/CI нет. Source markers + watch checks восстановлены. Чужие runtime data и checkout changes сохранены. +- Docker disk был заполнен. После адресной очистки intermediates этой задачи + и отдельно разрешённой очистки unused build cache старше суток проверки + продолжены; чужие images/containers/volumes не удалялись. + +### Оставшиеся внешние gates + +- 1.2: server Docker/Compose, Docker capability runner `ubuntu-latest`, + раздельные package writer и server pull-only authenticated login. +- 2.2/3.2: реальные Gitea candidate workflows и публикация frontend/backend, + OCI revisions, registry digests, clean pull и отсутствие credentials в logs. + Нужны Actions secrets `REGISTRY_USERNAME`/`REGISTRY_TOKEN` в каждом component + repo и отдельная server identity. Workflow не публикует `latest` и fail-closed + отказывается от overwrite или недоказанного отсутствия tag. +- 6.3: smoke опубликованной пары version tags на disposable server config/data, + запись installed image references/revisions до первого server rollout. + Server rollout, Caddy, TLS и реальный финансовый запрос не выполнялись. + +### Упрощение deployment configuration по запросу владельца + +Обязательные `FRONTEND_DIGEST`/`BACKEND_DIGEST` и отдельные repository fields +заменены на `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags. +Design и runbook синхронизированы. Результаты выше описывают первоначальную +проверку; обязательный digest pinning более не является deployment contract. +Migration и backend по-прежнему используют один image reference. + +Verification упрощения: production/development `docker compose config` PASS; +разные version tags выбираются независимо, migration/runtime получают один +backend image, один loopback port и development volume сохранены. Пустые +image/secret values блокируют config. Docs `npm run verify`, OpenSpec strict +change validation и `git diff --check` в affected repos — PASS. + +По следующему запросу владельца из `FROM` удалены base image digests: +используются `node:24.21.0-bookworm-slim` и `nginx:1.28.0-alpine`. +Version tags не гарантируют неизменность содержимого базового image. +Verification: повторная сборка production/development targets обоих компонентов +— PASS (с использованием Docker cache). Dockerfile остаются в component repos, +чтобы standalone build и release CI не зависели от root checkout. + +По запросу владельца удалены дополнительные commit tags из обоих workflows; +release tag защищён от перезаписи, revision остаётся в OCI label. Production +Compose наследует healthchecks из images; development содержит собственные +checks с прежними таймингами, migration отключает check. +Verification: production/development Compose config и invariants, healthchecks +локальных production images, shell syntax обоих workflows, docs verify, +OpenSpec strict validation и diff checks — PASS. Registry publication остаётся +внешней непроверенной частью задач 2.2/3.2. -- 2.54.0