Files
docs/architecture/containers.md
codex-bot 37430da4c9
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
docs: describe concentration tab and stored instrument classification (WealthLedger/main#48) (#60)
2026-11-01 19:29:01 +03:00

19 KiB
Raw Permalink Blame History

Приложения, хранилище и взаимодействия

Представление C4 Container раскрывает внутреннее устройство WealthLedger. Контейнер здесь означает приложение или хранилище, а не Docker-контейнер. Внешние системы совпадают с System Context.

Представление Container

flowchart LR
  U["Частный инвестор<br/>Человек"]
  subgraph W["WealthLedger · программная система"]
    F["React SPA<br/>TypeScript, Vite, Mantine, TanStack Query"]
    B["Backend API<br/>NestJS, Node.js"]
    D[("Локальная база<br/>Embedded SQLite")]
    F -->|"HTTP JSON /api/v1; Bearer и auth cookie"| B
    B -->|"Prisma + better-sqlite3; доступ к локальному файлу"| D
  end
  T["T-Bank Invest API<br/>Внешняя система"]
  M["MOEX ISS<br/>Внешняя система"]
  N["Реестр НКР<br/>Внешняя система"]
  C["T-Bank Brands CDN<br/>Внешняя система"]
  U -->|"Просматривает данные и управляет подключением"| F
  B -->|"gRPC over TLS; token пользователя"| T
  B -->|"HTTPS; публичные данные"| M
  B -->|"HTTPS; публичный HTML"| N
  F -->|"HTTPS; изображения без авторизации"| C

SPA выполняется в браузере пользователя. Backend — отдельное приложение; SQLite используется внутри его процесса через библиотеку и файл базы. Между API и SQLite нет сетевого database server. Граница системы на схеме показывает принадлежность частей, а не их размещение на одном узле.

Контейнерная поставка

Deployment view для одного сервера. Caddy принадлежит серверу; Compose содержит два постоянных application containers и одноразовую migration.

flowchart LR
  Browser["Браузер · React SPA"] -->|HTTPS| Caddy["Серверный Caddy · TLS"]
  subgraph Host["Один Docker host"]
    Caddy -->|"HTTP · 127.0.0.1:8080"| Frontend["frontend · nginx · SPA + /api/"]
    Frontend -->|"HTTP /api/v1 · internal network"| Backend["backend · NestJS · один process"]
    Migration["migration · Prisma · one-shot"] -->|"До старта backend"| Data[("Host directory · SQLite + WAL")]
    Backend -->|"Чтение и запись /data"| Data
    Secrets["Restricted environment · ключи вне images"] --> Backend
  end

Только frontend публикует host port, привязанный к loopback. Backend не публикует порт, но имеет исходящий доступ к внешним API. nginx сохраняет /api/ prefix, forwarding headers и request ID; Docker DNS разрешается повторно после backend recreation. HTML требует revalidation, hashed assets имеют immutable cache. Caddy не содержит отдельного маршрута к API.

Development заменяет nginx на Vite с HMR, backend — на Nest watch, а host data path — на отдельный named volume. Browser origin остаётся единым. Linux node_modules не монтируются с хоста. Production SPA собирается с relative API base; production cookies остаются Secure, публичный origin использует HTTPS.

Компоненты имеют независимые images и версии. Production фиксирует image version tags; migration использует тот же backend image. Успех migration предшествует backend healthcheck на существующем auth route; frontend ждёт healthy backend. Runtime работает non-root, SQLite сохраняется вне image, ключ шифрования и JWT secret поступают из restricted environment и сохраняются между обновлениями.

Ограничение SQLite расширено в ADR 015. Runbook описывает установку, independent updates, backup, rollback и explicit restore. Он также перечисляет внешние prerequisites: локальный smoke не подтверждает registry/server rollout.

Ответственность частей

Часть Ответственность
React SPA Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений
Backend API Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников
SQLite Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work
Внешние системы Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности

Frontend организован слоями app → pages → features → entities → shared: каркас связывает маршруты, страницы собирают сценарии, а общие средства обслуживают транспорт и представление. Backend остаётся модульным NestJS API, где локальная аутентификация отделена от подключения брокера и инвестиционных сценариев. Это внутренняя организация приложений, не набор микросервисов.

Маршруты и AppShell принадлежат app. Page controller/VM управляет запросами, формой и восстановлением; presentation получает данные и callbacks, поля могут использовать контекст React Hook Form. В OperationForm controller сохраняет intent операции, черновик и явное принятие актуальной версии после конфликта. В account-income чистые преобразования периода принадлежат модели страницы и не связывают форму с её запросами.

Локальные packages tokens/ui/icons образуют визуальную основу внутри frontend и входят в сборку SPA. Это библиотеки, а не дополнительные runtime-контейнеры. Провайдеры темы принадлежат приложению и Storybook; предметные компоненты и API-клиент остаются в приложении. Отдельный API-пакет и registry-поставка визуальных пакетов не реализованы. Существующие Docker images приложения не означают независимого npm-выпуска этих библиотек.

Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token T-Bank в форме и отправляет его для сохранения, но API не возвращает token в ответах; SPA не использует его для прямых брокерских запросов. Прямая загрузка публичного изображения из CDN не проходит через эту финансовую границу.

Типизированный frontend-клиент производится из предоставленного OpenAPI snapshot во время разработки. В runtime SPA вызывает API, а не генератор или OpenAPI endpoint. Форма запросов и ошибок описана в HTTP API; направление генерации принято в ADR 002, а доверие frontend к предоставленному snapshot — в ADR 014.

Хранимые данные и границы доступа

SQLite содержит пользователя с нормализованным email и hash пароля, его сессии с hash текущего refresh token, сроком действия и признаком отзыва, а также одно зашифрованное подключение T-Bank с временем подключения. Пароли хешируются bcrypt; token брокера шифруется AES-256-GCM. Plaintext credentials не являются сохраняемым представлением этих данных.

Ручной банковский учёт хранит карты, пользовательские категории и операции в отдельных таблицах, связанных с User. Начальный остаток и суммы записаны целыми bigint-копейками, календарные даты — днями без времени. Перевод хранится одной операцией с двумя картами; возврат связан с исходной покупкой. Баланс вычисляется из неудалённой истории и отдельно не сохраняется.

Банковские запросы проверяют локального владельца каждой связи и не используют подключение брокера. Транзакции, ограничения уникальности и версии согласуют переводы, возвраты и конкурирующие исправления. Fingerprint исходного ввода и отметка удаления защищают от повторного создания после потерянного ответа или перезапуска. Архив обратим и сохраняет историю. Подробности — банковские карты.

Сессии и подключение связаны с владельцем. Backend локально проверяет membership текущей revision перед каждым финансовым чтением. Unknown membership не открывает доступ к произвольному счёту; известный отказ токена блокирует личные данные. Ротация refresh token остаётся атомарной.

SQLite хранит полный последний portfolio, операции и их подтверждённые диапазоны, private прогнозы, сохранённые точки истории оценки, общие instrument/market sources, классификацию инструментов (сектор и бренд актива как последнее известное значение с временем каждого получения, TbankInstrumentClassification) и intent/staging. Нормализованные финансовые значения сохраняются decimal-строками. Полнота отделена от свежести и пагинации. Ответ не ждёт внешних RPC; обновлением личных данных владеет часовой обход в том же backend process через существующий лимитер, недостающие общие источники он же ставит в фоновую очередь. На startup обход начинает следующую попытку по обычному часовому сроку; после нового подключения первая попытка начинается сразу. Классификация — публичные данные без привязки к пользователю и подключению; её поля обновляются в том же обходе не чаще раза в сутки (ADR 022). Подробности — модель свежести.

PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией сохраняет encrypted token, новую revision и проверенный membership. Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат личных позиций, ожидаемых сумм или credentials. Произвольного архива отключённых счетов нет.

Сохранение и восстановление данных

Поддерживаемая процедура останавливает backend и копирует весь data directory с возможными WAL/SHM files до migration. Копирование отдельных файлов работающей базы не сохраняет целостный снимок; для live backup потребовался бы SQLite backup API. Восстановление проверяется на отдельной копии. Ключ шифрования подключения хранится отдельно от базы; его потеря делает сохранённый T-Bank token нечитаемым.

Откат приложения требует совместимости предыдущей версии с сохранённой схемой. Расширенная банковскими таблицами схема сохраняет пользователей, сессии и подключения. Возврат старого backup поверх новой истории удалил бы введённые банковские операции; откат приложения не равнозначен откату данных. Совместимость произвольных будущих версий этим не гарантируется.

Стратегия решения и её основания

Локальная база и Prisma дают типизированный доступ к локальному состоянию без отдельного сервера базы; исходные причины и пределы выбора сохранены в ADR 001. Централизованная серверная граница T-Bank сосредоточивает credentials, лимиты и повторы вне браузера, а шифрование подключения задаёт способ хранения брокерского секрета.

Политика сессий разделяет короткоживущий access token и вращаемый refresh token. Политика query-кэша сохраняет доступные данные при обновлении и не запускает refetch при возврате фокуса. Ограниченные повторы T-Bank и структурированные журналы обслуживают внешнюю границу. ADR сохраняют контекст принятия решения; текущие детали уточняются в тематических описаниях.

Качество и ограничения

Точность представления денег требует сохранять валюту и decimal-значение; неизвестная сумма не заменяется нулём. Свежесть наблюдения не равна актуальности рынка в реальном времени. Временные ошибки внешнего поставщика обрабатываются ограниченными повторами и локальными состояниями интерфейса, без обещания успешного ответа при недоступном upstream.

Авторизация доступа, недопущение token в публичный ответ и атомарность ротации сессии — отдельные свойства; наличие типизированного клиента не доказывает их. Действующий формат ошибок описан в HTTP API.

Поддерживается локальный Compose и ограниченная single-server topology с SQLite. Origin браузера, адрес API и cookie policy должны быть согласованы. localhost и 127.0.0.1 задают разные browser-границы для cookie и CORS. Локальный HTTP поддерживается в development-режиме backend; в остальных режимах refresh-cookie имеет Secure flag и требует защищённого соединения. Single-server deployment не задаёт гарантий HA или zero-downtime.

Coordinator и лимитер T-Bank действуют на один экземпляр backend. Сохранённые данные переживают перезапуск; processRunId не является distributed lease. Ротация ключа шифрования подключений и согласование изменений подключения между распределёнными экземплярами не входят в реализованный механизм. Эти ограничения не позволяют переносить локальную схему на несколько экземпляров без отдельного решения о состоянии, credentials и лимитах.

Runtime-сценарии раскрываются у соответствующих владельцев: восстановление сессии, сохранение подключения и загрузка портфеля.