From 718365cd5ab51e7c696c777b71095f7ee41b471d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 05:42:14 +0300 Subject: [PATCH 1/5] docs: refine frontend conventions and routing --- .ai/CONVENTIONS.md | 28 ++------ .ai/SOURCES.md | 2 +- .ai/architecture.md | 34 ++++++++++ .ai/conventions/frontend-architecture.md | 21 +++--- .ai/conventions/frontend-data.md | 22 ++++++- .ai/conventions/frontend-testing.md | 5 ++ .ai/conventions/frontend-ui.md | 82 +++++------------------- .ai/conventions/storybook.md | 33 ++++++++++ .ai/conventions/typescript-react.md | 53 +++++++++++++++ AGENTS.md | 56 +++++++--------- 10 files changed, 202 insertions(+), 134 deletions(-) create mode 100644 .ai/architecture.md create mode 100644 .ai/conventions/storybook.md create mode 100644 .ai/conventions/typescript-react.md diff --git a/.ai/CONVENTIONS.md b/.ai/CONVENTIONS.md index 5b5becd..8dec1c0 100644 --- a/.ai/CONVENTIONS.md +++ b/.ai/CONVENTIONS.md @@ -1,23 +1,9 @@ -# Frontend router +# Frontend conventions: вход -Читай строки, которые активирует задача, затронутый путь **или** обнаруженная -граница. Объединяй совпадения; якорь разрешает чтение только нужного раздела. -Не загружай все conventions из-за одного frontend-файла. +Актуальный routing по задаче, пути и boundary находится в [AGENTS](../AGENTS.md). +Выбирай только активированные темы; при новой границе повтори routing. +[Architecture](architecture.md) объясняет mental model, [SOURCES](SOURCES.md) +ведёт к системным фактам, specifications и ADR по конкретному вопросу. -| Решение / сигнал | Читать до решения | -| --- | --- | -| Создать/перенести module/component/slice; выбрать page/entity/feature/shared; cross-slice imports | [Architecture](conventions/frontend-architecture.md) | -| Component/props, container/VM/view, form, local state/effect, loading/error/retry, Mantine/UI | [UI](conventions/frontend-ui.md) | -| Только продуктовая строка или Zod message | [Строки](conventions/frontend-ui.md#строки-и-валидация) | -| TS declaration/import/export, domain literals, magic values | [Код](conventions/frontend-ui.md#код-и-components) + [Domain literals](conventions/frontend-ui.md#domain-literals-and-magic-values) | -| query/mutation, shared/api, server state/cache, generated types, Bearer/refresh | [Data](conventions/frontend-data.md) | -| Тесты или behavior с testing risk | [Testing](conventions/frontend-testing.md) | -| Новый abstraction/dependency, refactor или неоднозначный scope | [Общий выбор реализации](../../AGENTS.md) | -| Request/response field, route/status/error shape, OpenAPI snapshot/generated type | [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) | -| Login/refresh/logout, token/cookie, auth ownership | Data + [security](../../backend/.ai/conventions/security-auth.md) + Testing | -| Storybook/theme/tokens/shared UI | UI + точные [источники](SOURCES.md) дизайн-системы/каталога | - -UI-only props/presentation требуют UI, без API/structure/testing при отсутствии -их triggers. Показ query state не означает изменение data fetching. -Изменение тестов активирует Testing и без production diff. Нужные project facts -выбирай через SOURCES. Повтори routing при новой границе и по итоговому diff. +Этот адрес сохранён для существующих входящих ссылок; второй копии правил +и таблицы маршрутов здесь нет. diff --git a/.ai/SOURCES.md b/.ai/SOURCES.md index 916b004..63570fd 100644 --- a/.ai/SOURCES.md +++ b/.ai/SOURCES.md @@ -2,7 +2,7 @@ | Что нужно установить | Владелец | | --- | --- | -| Какое implementation-решение принять | [CONVENTIONS](CONVENTIONS.md) | +| Какое implementation-решение принять | [AGENTS](../AGENTS.md) | | HTTP wire contract и направление генерации | Backend-owned contract → provided snapshot → [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) | | Error codes/validation/error envelope | [docs: HTTP API](../../docs/architecture/http-api.md) | | Auth/session, token storage и refresh | [Аутентификация](../../docs/domains/authentication/overview.md), [подключение брокера](../../docs/integrations/tbank.md) | diff --git a/.ai/architecture.md b/.ai/architecture.md new file mode 100644 index 0000000..12bb7b3 --- /dev/null +++ b/.ai/architecture.md @@ -0,0 +1,34 @@ +# Frontend mental model + +React + TypeScript + Vite SPA. Router задаёт navigation; providers в `app` +подключают auth, TanStack Query, i18n и Mantine. Vite использует React Compiler; +Vitest — отдельную JSDOM конфигурацию. Точные versions/scripts — в +[package.json](../package.json), реализация — source of truth. + +```text +app → pages → features → entities → shared +page container → page VM → entity/feature query → shared/api → backend /api/v1 + ↓ + props → state/section views +``` + +`widgets` не используется. Pages владеют route composition и локальным поведением; +features — самостоятельными возможностями пользователя; entities — устойчивыми +доменными понятиями; shared — техническими primitives без domain responsibility. +Правила выделения owner и public `index.ts` — в +[FSD convention](conventions/frontend-architecture.md). + +Server state и lifecycle запроса принадлежат TanStack Query; VM собирает данные +для view. Transport — `openapi-fetch`/`openapi-react-query`, типизированные из +предоставленного snapshot через `openapi-typescript`. Backend владеет wire +contract; frontend не читает его checkout для генерации и не вызывает T-Bank. +[Data convention](conventions/frontend-data.md) задаёт auth/cache границы. + +`shared/theme/tokens.json` — переносимый source design tokens; `theme.ts` +адаптирует доверенный JSON к Mantine без runtime schema. App и Storybook читают +public API `shared/theme`. Storybook показывает synthetic presentation states, +не использует application API. RTL/MSW проверяют поведение приложения отдельно. + +Existing route-aware AppShell расположен в shared/ui; это legacy placement, +а не образец нового shared primitive. Общий план улучшений читается только +при migration через [router](CONVENTIONS.md). diff --git a/.ai/conventions/frontend-architecture.md b/.ai/conventions/frontend-architecture.md index ec83d4a..5983689 100644 --- a/.ai/conventions/frontend-architecture.md +++ b/.ai/conventions/frontend-architecture.md @@ -27,20 +27,17 @@ Slice именуется по ответственности, а не `helpers`, Cross-slice consumer использует `@/` и `index.ts` владельца, например `@/entities/portfolio`. Alias Vite и TypeScript согласован. Другой slice не -импортирует внутренний файл напрямую. Направление зависимостей проверяет +импортирует внутренний файл напрямую. Slices `pages`, `features`, `entities` не зависят от соседнего slice своего слоя; +композиция принадлежит вышележащему owner. Existing меж-slice исключения +не являются разрешением добавлять новые. + +Направление зависимостей и public imports частично проверяет `npm run deps`; Knip проверяет использование exports, а не смысл placement. ## UI foundation -Общий visual foundation принадлежит `shared/theme`. `tokens.json` — переносимый -WealthLedger v1 source; `theme.ts` адаптирует доверенный JSON в Mantine без runtime -schema. App и Storybook используют public API `shared/theme/index.ts`: theme, -designTokens, color roles, CSSVariablesResolver и base styles. Внешний consumer -читает JSON напрямую. Mantine defaults и локальные domain styles остаются у владельцев. -`shared/ui` содержит StateView/LoadingSkeleton. Existing route-aware AppShell также -лежит в shared/ui, но не является общим catalog primitive; его расположение не -служит основанием переносить новую domain/navigation логику в shared. -Изменяя foundation, прочитай [design system](../../../docs/design/overview.md). +Mental model, theme и data flow — в [architecture](../architecture.md). +При изменении foundation прочитай [design system](../../../docs/design/overview.md). ## Примеры @@ -52,5 +49,7 @@ designTokens, color roles, CSSVariablesResolver и base styles. Внешний c После placement/import change: `npm run deps`, `npm run typecheck`, `npm run knip`; после собственного behavior — применимые tests. Финальный gate указан в AGENTS. -Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает +Existing dependency config не запрещает все same-layer imports: их и смысл +owner проверяет review. Не объявляй Steiger установленным; graph проверяет +текущий dependency-cruiser. Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает правильный выбор слоя. diff --git a/.ai/conventions/frontend-data.md b/.ai/conventions/frontend-data.md index 1b1428b..1764a00 100644 --- a/.ai/conventions/frontend-data.md +++ b/.ai/conventions/frontend-data.md @@ -23,7 +23,9 @@ type User = paths["/api/v1/auth/me"]["get"]["responses"][200]["content"]["applic ``` Domain/UI type допустим, когда он отличается по смыслу, а не копирует transport DTO. -Generated TS вручную не исправляется. Frontend-команды не читают соседний +Generated TS вручную не исправляется. Не добавляй параллельный fetch/axios +client, runtime schema, hash/checksum registry или дополнительный drift checker +без отдельной текущей потребности; existing OpenAPI flow уже владеет генерацией. Frontend-команды не читают соседний backend checkout и не проверяют backend runtime OpenAPI; изменение wire shape/status/route сохраняет [границу HTTP-контракта](../../../docs/architecture/http-api.md#источник-контракта). @@ -44,11 +46,29 @@ session-scoped cache. При auth/token change читай [security](../../../ba GET subtree; ошибка mutation не меняет generation/cache. Это отдельная граница от обычного фонового refetch: [connection policy](../../../docs/integrations/tbank.md#граница-финансового-кэша). +Mutations используют существующий generated client и query-key owner; +после успеха обновляй/invalidируй только связанные данные. Optimistic update +нужен конкретному UX и обязан иметь rollback/error behavior. + Default focus refetch выключен; freshness и retry определяются существующей [query policy](../../../docs/adr/007-react-query-fetch-policy.md) и feature source. При обычном refetch сохраняй Client Cached Snapshot, при session/connection смене не показывай данные прежнего владельца. UI presentation — в UI convention. +## Errors + +Извлекай code через existing `apiErrorCode` из shared/api. Backend Problem +Details code задаёт machine behavior; user-facing текст принадлежит i18n. +Network/unknown error получает безопасное локальное сообщение и retry, +не raw exception text. Input/domain failure сохраняет ввод и показывает +ошибку у ответственного form/section owner; не маскируй её успешным результатом. + +Если одна группа API codes требует одинакового recovery в нескольких views, +сведи классификацию у ближайшего общего owner. Не размножай magic comparisons +и не объявляй закрытый frontend error enum: generated `code` пока `string`. +Неизвестный новый code должен сохранять fallback. Рефакторинг существующих +разных recovery paths требует behavior proof, а не общей обработки всех ошибок. + ## Примеры и проверка [usePortfolio](../../src/entities/portfolio/api/usePortfolio.ts) — entity query; diff --git a/.ai/conventions/frontend-testing.md b/.ai/conventions/frontend-testing.md index 143be13..8b3513a 100644 --- a/.ai/conventions/frontend-testing.md +++ b/.ai/conventions/frontend-testing.md @@ -1,5 +1,9 @@ # Frontend testing +Перед нетривиальными тестами сформулируй +[общий Test Intent](../../../.ai/conventions/testing.md); отдельный документ +не требуется. Trivial low-risk изменения обходятся без него. + ## Выбор проверки Читай при изменении тестов, доступа, денежных расчётов, восстановления @@ -11,6 +15,7 @@ | Чистое domain преобразование/денежный расчёт | Unit case с граничными значениями | | Форма, route access, recovery, UI-state selection | RTL component/route test через видимое поведение | | Headers, refresh/retry, query invalidation | MSW на transport boundary | +| Критический browser flow, который JSDOM не доказывает | Browser/e2e proof существующими доступными средствами; отдельный frontend e2e runner пока не принят | | Только declarative props/Storybook fixture | Type/build + semantic/visual review; unit test без собственной логики не нужен | Tests colocated с source (`*.test.tsx` для UI, существующий `*.test.ts` для pure code). diff --git a/.ai/conventions/frontend-ui.md b/.ai/conventions/frontend-ui.md index c211935..f1c13f4 100644 --- a/.ai/conventions/frontend-ui.md +++ b/.ai/conventions/frontend-ui.md @@ -11,6 +11,11 @@ Mantine component и общую тему. Для выпадающего выбо ограничения JSDOM решай в test setup, сохраняя production UI. Семантический HTML для структуры страницы остаётся уместным. +Icons ищи у существующего owner (например, +[BankOperationIcon](../../src/entities/bank-card/ui/BankOperationIcon.tsx)); +общая icon library сейчас не принята. Не добавляй пакет ради одного символа. Business/domain +components живут у feature/entity/page, даже при повторном использовании. + Wrapper нужен для проектной ответственности, а не ради нового имени для Button. Локальный CSS module принадлежит component; общий token вводи для реально общей роли через [design system](../../../docs/design/overview.md). @@ -23,9 +28,11 @@ Wrapper нужен для проектной ответственности, а | Локальное открытие, focus, rendering fallback | React UI state | | Значение, вычисляемое из уже доступных inputs | Вычисление при render/в VM, без копирования через effect в state | -Effect нужен для синхронизации с внешней системой; действие пользователя -обрабатывай в handler. Не добавляй store, memoization или abstraction без текущей -потребности. Inspect ближайший component до создания нового pattern. +State, который нужен в ссылке, back/forward или после navigation, хранится в URL +через existing router search params. Остальной UI state остаётся у ближайшего +owner. Shared client store нужен только нескольким consumers без подходящего +общего owner; наличие двух компонентов само по себе его не требует. +Hooks/effects/derived state — по [TypeScript/React](typescript-react.md#hooks-и-derived-state). ## Container, VM и view @@ -76,71 +83,12 @@ Button loading при mutation и компактный cached-snapshot progress ## Код и components -Style/type automation задают configs. Используй `import type`; `enum` и -`namespace` в frontend не применяются. Named components — const-valued; Biome -проверяет declarations, no-default-export и exports-last в своём scope. - -Обязательный review: `FC` и named props рядом/в types-файле; без inline -props typing и destructuring в параметрах. Принимай `props`, destructure в body. -Только children — `FC`; дополнительные props — named type -на основе `PropsWithChildren<{ ... }>`. Без props — `FC`. -Своди обычные exports к читаемому нижнему named block; barrel делает прямой -re-export без промежуточного import. - -CSF `*.stories.tsx` сохраняют default meta и прямые story exports для indexer. -Vite/Vitest configs и `.storybook/main.ts`/`preview.tsx` имеют только -конфигурационное default-export exception. Scope исключений принадлежит biome.json. +Types, props, hooks и memoization — в [TypeScript/React](typescript-react.md). ## Domain literals and magic values -Finite business vocabulary must be modeled explicitly; incidental implementation -literals stay inline. Это относится к error codes, statuses, operation/account/ -instrument/transaction types, permissions, roles, feature states и бизнес-режимам. - -Перед новым определением найди existing owner/type/value. В branching и comparisons -используй символическое значение владельца; не повторяй raw domain literals. -Храни словарь у domain/module/entity с соблюдением границ слоёв, без глобальной -свалки `constants.ts`. Сохраняй принятый pattern; для нового project-owned словаря -без подходящего аналога используй `as const` с derived union: - -```ts -const BankErrorCode = { - InvalidDate: "BANK_INVALID_DATE", - ImmutableOperation: "BANK_IMMUTABLE_OPERATION", -} as const; -type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode]; - -// Вместо code === "BANK_INVALID_DATE": -if (code === BankErrorCode.InvalidDate) { /* existing handling */ } -``` - -Значимые numeric policies (timeout, retry, page size, threshold, coefficient) -называй по смыслу и единицам: `REQUEST_TIMEOUT_MS`, `CACHE_TTL_SECONDS`, -`MAX_RETRY_COUNT`, `PAGE_SIZE`. Оставляй inline UI labels в принятом месте, -log messages, индексы, `index + 1`, `Math.max(value, 0)`, `page ?? 1`, -`percentage * 100` и очевидные локальные детали без semantic benefit. -При неясном owner/смысле оставь значение и укажи ограничение в review. - -Не мигрируй корректные existing enum/union styles массово. Не добавляй ради -правила factories, registries, schemas, loaders, abstraction layers, dependencies, -custom lint/AST rules, generation/validation scripts или тесты существования -констант. Semantic review проверяет ownership, единое написание и неизменность -wire values; обычные behavior tests проверяют поведение. - -Backend API vocabulary бери из generated OpenAPI types/client, runtime values — -когда генератор их предоставляет. Не создавай ручную копию backend enum/object. -Текущий `schema.d.ts` предоставляет только types: используй indexed-access type -из `paths`; typed literals в запросах/сравнениях допустимы при отсутствии -runtime values. `ProblemDetailsDto.code` сейчас `string`: не выдумывай закрытый -frontend error enum. Развитие API source — отдельное contract-решение. -Frontend `enum` по-прежнему запрещён локальным TS/style profile. - -`noMagicNumbers` включён как error в Biome. Fixtures, tests и stories исключены -из этого правила; остальные проверки для них сохраняются. Используй имена -политик/единиц и existing values. Точечное подавление допустимо для очевидного -incidental значения с объяснением, например перевода доли в проценты; не подавляй -скрытые бизнес-лимиты. Semantic magic strings проверяются conventions/review: в -установленном Biome нет такого встроенного правила, новый инструмент не добавляем. +Правило и generated API exception — в +[TypeScript/React](typescript-react.md#domain-literals-and-magic-values). ## Строки и валидация @@ -154,5 +102,5 @@ incidental значения с объяснением, например пере Type/style: `npm run typecheck`, `npm run lint`. Для risk behavior/изменения тестов используй [Testing](frontend-testing.md). UI: keyboard, narrow viewport, -loading/error/empty/loaded и recovery по применимости. [Storybook](../../README.md#ui-каталог-storybook) -уже установлен; используй его как reference, но сверяй UI с задачей отдельно от build. +loading/error/empty/loaded и recovery по применимости. [Storybook](storybook.md) задаёт выбор stories и изоляцию каталога; +сверяй UI с задачей отдельно от build. diff --git a/.ai/conventions/storybook.md b/.ai/conventions/storybook.md new file mode 100644 index 0000000..c82e9d0 --- /dev/null +++ b/.ai/conventions/storybook.md @@ -0,0 +1,33 @@ +# Storybook + +## Когда нужна story + +- Shared UI/theme: показывай принятые visual roles и переиспользуемое поведение. +- Domain/page presentation: добавляй story, когда независимый visual state + нужен для разработки/review или является устойчивым UI pattern. +- Выбирай значимые состояния: default, loading, disabled, error, empty, + длинный контент и смысловые variants по применимости. Не строй декартово + произведение props и story для каждого trivial wrapper/helper. +- Fetch/auth/router container и VM проверяй application tests; story получает + synthetic props presentation component. Каталог не нужен каждому файлу. + +## Изоляция и interaction + +Используй CSF, `Meta`/`StoryObj`, `satisfies Meta`, локальные fixtures и actions. +Общие Mantine/theme/i18n/Dates providers уже есть в +[preview](../../.storybook/preview.tsx); не создавай второй theme source. +Stories не достигают application API, auth, VM и query providers даже транзитивно; +границу проверяет `deps:storybook`. Не добавляй MSW worker ради каталога. + +`play` через `storybook/test` нужен, когда interaction иллюстрирует собственный +контракт: keyboard activation, доступность действия, local callback. Не повторяй +RTL suite и не тестируй внутренности Mantine. Существующий +[Button](../../src/shared/ui/Button.stories.tsx) показывает themed control +без production wrapper; [LoadingSkeleton](../../src/shared/ui/LoadingSkeleton.stories.tsx) +— значимые busy states. + +`npm run typecheck`, `npm run deps:storybook`, `npm run build:catalog` проверяют +типы, изоляцию и сборку. Build не выполняет `play` как CI interaction suite; +для проверки interaction открой story в браузере, для regression используй +существующий [Testing](frontend-testing.md). Визуальное соответствие задаче, +keyboard и узкий viewport проверяются отдельно от успешной сборки. diff --git a/.ai/conventions/typescript-react.md b/.ai/conventions/typescript-react.md new file mode 100644 index 0000000..fee0393 --- /dev/null +++ b/.ai/conventions/typescript-react.md @@ -0,0 +1,53 @@ +# TypeScript и React + +## Types и components + +- Сохраняй strict TS profile и `import type`. Для недоверенного значения используй + `unknown` с narrowing; `any` не заменяет DTO/validation. Локальное исключение + на нетипизированной boundary должно быть узким и объяснённым. +- `enum` и `namespace` не используются: frontend имеет `erasableSyntaxOnly`. + Для состояния с разными payload выбирай discriminated union; не добавляй + необязательные поля, допускающие невозможные комбинации. +- Named components — const-valued `FC`; named props находятся рядом или + в types-файле. Принимай `props`, destructure в body, без inline props typing. + Только children — `FC`; дополнительные props — named type + на основе `PropsWithChildren<{ … }>`; без props — `FC`. +- Обычные exports — нижний named block; barrel делает прямой re-export. + CSF сохраняет default meta и прямые story exports; configuration exceptions + заданы в [Biome](../../biome.json). Не распространяй их на application code. + +## Hooks и derived state + +Hooks вызываются без условий и ранних выходов перед ними; custom hook нужен +связному поведению, а не новому имени над одним вызовом. Derived value вычисляй +при render/в VM. Пользовательское действие обрабатывай handler; `useEffect` +оставляй синхронизации с внешней системой с корректным cleanup/dependencies. + +`useMemo`, `useCallback`, `memo` нужны измеренной стоимости или конкретному +контракту стабильной identity; не добавляй их ко всем значениям/handlers. +React Compiler включён в Vite, но не доказывает выигрыш и не заменяет effect +correctness. Не удаляй существующую memoization без понимания её consumers. +Container/VM/view и state owners — в [UI](frontend-ui.md). + +## Domain literals and magic values + +Общее правило ownership и символических значений — +[Domain values](../../../.ai/conventions/code-health.md#domain-values). +Backend vocabulary бери из generated `paths`, не создавай ручную копию enum. +Текущий `schema.d.ts` содержит только types: typed wire literals допустимы, +когда generator не предоставляет runtime values. + +`ProblemDetailsDto.code` сейчас `string`. Не объявляй закрытый frontend error +enum и не превращай неизвестный code в известный через cast. Повторяющуюся +обработку своди у ответственного owner; [errors](frontend-data.md#errors). + +`noMagicNumbers` в Biome — error; fixtures/tests/stories исключены только +из этого правила. Policy получает имя и единицы; подавление допустимо для +incidental value с объяснением. Обычный UI-текст не превращай в constant: +его owner — [i18n](frontend-ui.md#строки-и-валидация). +Semantic magic strings и props profile требуют review. + +## Проверка + +`npm run typecheck` и `npm run lint`; behavior проверяй по +[Testing](frontend-testing.md), placement — по [FSD](frontend-architecture.md). diff --git a/AGENTS.md b/AGENTS.md index f215523..3d9dff9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,36 +1,26 @@ -# Frontend +# Frontend router -React/Vite-клиент; общие правила работы — в root `AGENTS.md`. -До frontend-решения прочитай [router](.ai/CONVENTIONS.md) и -[sources](.ai/SOURCES.md), затем только применимые документы/разделы. +Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, +путей и обнаруженных boundaries; читай только нужные темы до решения. +[Sources](.ai/SOURCES.md) выбирает факты/spec/ADR по возникшему вопросу. -## Проверка +| Решение / сигнал | Читать | +| --- | --- | +| Первое знакомство, data flow, общая архитектура | [Mental model](.ai/architecture.md) | +| Создать/перенести component/module/slice, owner/FSD, cross-slice imports | [FSD](.ai/conventions/frontend-architecture.md) | +| TS types/props/imports/exports, hooks/effects, derived state, memoization, domain literals | [TypeScript/React](.ai/conventions/typescript-react.md) | +| UI/Mantine, form, container/VM/view, state owner, loading/error/recovery | [UI](.ai/conventions/frontend-ui.md) | +| Только продуктовая строка / Zod message | [Строки](.ai/conventions/frontend-ui.md#строки-и-валидация) | +| Query/mutation, server cache, shared/api, generated types | [Data](.ai/conventions/frontend-data.md) | +| API/network/application errors, codes и сообщения | [Errors](.ai/conventions/frontend-data.md#errors) | +| Login/refresh/logout, token/cookie, session ownership | Data + [Security](../backend/.ai/conventions/security-auth.md) + [Testing](.ai/conventions/frontend-testing.md) | +| Request/response field, route/status/error shape, OpenAPI | Data + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | +| Tests или behavior с regression risk | [Testing](.ai/conventions/frontend-testing.md) | +| Storybook/stories/play, theme/tokens/shared UI | [Storybook](.ai/conventions/storybook.md), UI; design facts — через Sources | +| Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | +| Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | -Перед Node-командами: `nvm use --silent` по `.nvmrc`. -После lockfile install `npm ci` полный gate — `npm run verify`. -Он проверяет OpenAPI drift, TypeScript, Biome, Vitest/coverage, Knip, -dependency boundaries и сборки приложения/Storybook без backend. -Точные scripts принадлежат `package.json`; targeted checks — у выбранного convention. - -HTTP contract changes используют предоставленный OpenAPI snapshot; frontend -локально проверяет только соответствие generated types этому snapshot: -[Источник HTTP-контракта](../docs/architecture/http-api.md#источник-контракта). -Generated types редактируются через их source; T-Bank доступен только через backend. -Frontend dependencies используют exact versions. Pre-commit запускает staged -formatting и FSD checks; сообщения commits — Conventional Commits. -Если у задачи есть связанная issue этого репозитория, добавляй её номер -в конец заголовка: `ci: run full frontend verification #1`. -Без связанной issue номер необязателен: `chore(deps): update non-major dependencies`. -Формат Conventional Commits проверяет `commitlint.config.cjs`. - -Code/style checks автоматические только в scope конфигурации. i18n, props, -semantic placement и выбор полезных тестов обязательны при review. -Storybook уже доступен как reference; ожидаемый UI и его состояния определяет -задача, а визуальное соответствие проверяется отдельно от сборки. - -## Ветки и релизы - -Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую -не пушить. Release/hotfix и синхронизация веток — по [root README](../README.md#релизы-и-ветки). -Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`. -Перед подготовкой релиза прочитай root README; версии вручную не повышать. +Показ query state не означает изменение fetching. Изменение тестов активирует +Testing и без production diff. Повтори routing по итоговому diff, включая новые +файлы. Migration backlog читается только при работе над соответствующим finding +через [plan](../.ai/migrations/conventions-v1/plan.md). -- 2.54.0 From 9f7e342333b80bfacfd5bc394a2dcd0613d33ff3 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 05:58:31 +0300 Subject: [PATCH 2/5] docs: simplify conventions layout --- .ai/CONVENTIONS.md | 9 --- .ai/SOURCES.md | 18 ----- .ai/architecture.md | 66 ++++++++++++++++--- .ai/conventions/{frontend-data.md => data.md} | 2 +- .ai/conventions/frontend-architecture.md | 55 ---------------- .ai/conventions/storybook.md | 2 +- .../{frontend-testing.md => testing.md} | 0 .ai/conventions/typescript-react.md | 8 +-- .ai/conventions/{frontend-ui.md => ui.md} | 4 +- AGENTS.md | 33 +++++++--- README.md | 10 +-- 11 files changed, 93 insertions(+), 114 deletions(-) delete mode 100644 .ai/CONVENTIONS.md delete mode 100644 .ai/SOURCES.md rename .ai/conventions/{frontend-data.md => data.md} (98%) delete mode 100644 .ai/conventions/frontend-architecture.md rename .ai/conventions/{frontend-testing.md => testing.md} (100%) rename .ai/conventions/{frontend-ui.md => ui.md} (98%) diff --git a/.ai/CONVENTIONS.md b/.ai/CONVENTIONS.md deleted file mode 100644 index 8dec1c0..0000000 --- a/.ai/CONVENTIONS.md +++ /dev/null @@ -1,9 +0,0 @@ -# Frontend conventions: вход - -Актуальный routing по задаче, пути и boundary находится в [AGENTS](../AGENTS.md). -Выбирай только активированные темы; при новой границе повтори routing. -[Architecture](architecture.md) объясняет mental model, [SOURCES](SOURCES.md) -ведёт к системным фактам, specifications и ADR по конкретному вопросу. - -Этот адрес сохранён для существующих входящих ссылок; второй копии правил -и таблицы маршрутов здесь нет. diff --git a/.ai/SOURCES.md b/.ai/SOURCES.md deleted file mode 100644 index 63570fd..0000000 --- a/.ai/SOURCES.md +++ /dev/null @@ -1,18 +0,0 @@ -# Источники frontend - -| Что нужно установить | Владелец | -| --- | --- | -| Какое implementation-решение принять | [AGENTS](../AGENTS.md) | -| HTTP wire contract и направление генерации | Backend-owned contract → provided snapshot → [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) | -| Error codes/validation/error envelope | [docs: HTTP API](../../docs/architecture/http-api.md) | -| Auth/session, token storage и refresh | [Аутентификация](../../docs/domains/authentication/overview.md), [подключение брокера](../../docs/integrations/tbank.md) | -| Значение Money, cached snapshot, account/position/income | [docs: Domain](../../docs/glossary.md) | -| Mantine/theme/tokens и существующие stories | [Дизайн-система](../../docs/design/overview.md), [UI-каталог](../README.md#ui-каталог-storybook) | -| As-built behavior нужной feature | [docs: Features](../../docs/README.md#предметные-области) | -| Существующие behavioral requirements, если нужны по задаче | [specs index](../../specs/AI_INDEX.md) | -| Действующие versions/scripts/check scope | package.json, lockfile, Biome/TS/dependency configs | - -Открывай точный source только по возникшему вопросу. Code examples показывают -pattern, но не переопределяют контракт. Specs не являются editable OpenAPI; -SDD-процедура для обычной работы не нужна. При конфликте источников сначала -установи факты, не меняй generated types или security policy по догадке. diff --git a/.ai/architecture.md b/.ai/architecture.md index 12bb7b3..1eb45fa 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -1,4 +1,6 @@ -# Frontend mental model +# Frontend architecture + +## Устройство и data flow React + TypeScript + Vite SPA. Router задаёт navigation; providers в `app` подключают auth, TanStack Query, i18n и Mantine. Vite использует React Compiler; @@ -12,17 +14,47 @@ page container → page VM → entity/feature query → shared/api → backend / props → state/section views ``` -`widgets` не используется. Pages владеют route composition и локальным поведением; -features — самостоятельными возможностями пользователя; entities — устойчивыми -доменными понятиями; shared — техническими primitives без domain responsibility. -Правила выделения owner и public `index.ts` — в -[FSD convention](conventions/frontend-architecture.md). - Server state и lifecycle запроса принадлежат TanStack Query; VM собирает данные для view. Transport — `openapi-fetch`/`openapi-react-query`, типизированные из предоставленного snapshot через `openapi-typescript`. Backend владеет wire contract; frontend не читает его checkout для генерации и не вызывает T-Bank. -[Data convention](conventions/frontend-data.md) задаёт auth/cache границы. +[Data convention](conventions/data.md) задаёт auth/cache границы. + +## Выбор владельца + +Сначала найди владельца поведения и ближайший пример. Расширяй existing slice, +если ответственность остаётся связной. Размер файла или возможный будущий reuse +не создают новую архитектурную границу. + +| Ответственность | Размещение | +| --- | --- | +| Bootstrap, глобальные providers, routes | `app` | +| Композиция маршрута и поведение, нужное только этому экрану | `pages/` | +| Самостоятельная пользовательская возможность с собственным смыслом | `features/` | +| Представление/поведение бизнес-сущности | `entities/` | +| Переиспользуемый код без WealthLedger domain meaning | `shared` | + +Локальный FSD profile: `app → pages → features → entities → shared`, без `widgets`. +Крупный уникальный блок остаётся в своей page. Form, mutation или click handler +сами по себе не требуют feature. Два callers не делают domain helper generic shared. +Slice именуется по ответственности, а не `helpers`, `hooks` или `types`. + +**Безопасный путь:** при сомнении оставь код у ближайшего допустимого owner. +Извлекай его, когда появилась самостоятельная ответственность или реальный reuse. +Не вводи новый layer и не обходи public API, чтобы устранить неудобный import. + +## Imports и public API + +Cross-slice consumer использует `@/` и `index.ts` владельца, например +`@/entities/portfolio`. Alias Vite и TypeScript согласован. Другой slice не +импортирует внутренний файл напрямую. Slices `pages`, `features`, `entities` не зависят от соседнего slice своего слоя; +композиция принадлежит вышележащему owner. Existing меж-slice исключения +не являются разрешением добавлять новые. + +Направление зависимостей и public imports частично проверяет +`npm run deps`; Knip проверяет использование exports, а не смысл placement. + +## UI foundation `shared/theme/tokens.json` — переносимый source design tokens; `theme.ts` адаптирует доверенный JSON к Mantine без runtime schema. App и Storybook читают @@ -31,4 +63,20 @@ public API `shared/theme`. Storybook показывает synthetic presentation Existing route-aware AppShell расположен в shared/ui; это legacy placement, а не образец нового shared primitive. Общий план улучшений читается только -при migration через [router](CONVENTIONS.md). +при migration через [router](../AGENTS.md). +При изменении foundation прочитай [design system](../../docs/design/overview.md). + +## Примеры + +- Общая busy-область: существующий [LoadingSkeleton](../src/shared/ui/LoadingSkeleton.tsx), а не domain widget в shared. +- Account-income state views: [страница](../src/pages/account-income/ui/AccountIncomePage.tsx) и её flat `components/*.view.tsx`; новый page-local filter остаётся здесь. +- Portfolio query: [entity public API](../src/entities/portfolio/index.ts); новая UI-сводка с бизнес-смыслом не становится shared только из-за нескольких экранов. + +## Проверка + +После placement/import change: `npm run deps`, `npm run typecheck`, `npm run knip`; +после собственного behavior — применимые tests. Финальный gate указан в AGENTS. +Existing dependency config не запрещает все same-layer imports: их и смысл +owner проверяет review. Не объявляй Steiger установленным; graph проверяет +текущий dependency-cruiser. Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает +правильный выбор слоя. diff --git a/.ai/conventions/frontend-data.md b/.ai/conventions/data.md similarity index 98% rename from .ai/conventions/frontend-data.md rename to .ai/conventions/data.md index 1764a00..b73d106 100644 --- a/.ai/conventions/frontend-data.md +++ b/.ai/conventions/data.md @@ -74,7 +74,7 @@ Network/unknown error получает безопасное локальное [usePortfolio](../../src/entities/portfolio/api/usePortfolio.ts) — entity query; [shared client](../../src/shared/api/client.ts) — transport/auth boundary. Изменяя header/retry/invalidation, проверяй наблюдаемое поведение через MSW и -[Testing](frontend-testing.md); не обращайся к реальному T-Bank. +[Testing](testing.md); не обращайся к реальному T-Bank. `npm run openapi:check`, `npm run typecheck` и targeted tests проверяют разные части решения. Для API semantics нужны exact docs и проверки наблюдаемого HTTP-поведения на стороне backend. diff --git a/.ai/conventions/frontend-architecture.md b/.ai/conventions/frontend-architecture.md deleted file mode 100644 index 5983689..0000000 --- a/.ai/conventions/frontend-architecture.md +++ /dev/null @@ -1,55 +0,0 @@ -# Frontend architecture - -## Выбор владельца - -Сначала найди владельца поведения и ближайший пример. Расширяй existing slice, -если ответственность остаётся связной. Размер файла или возможный будущий reuse -не создают новую архитектурную границу. - -| Ответственность | Размещение | -| --- | --- | -| Bootstrap, глобальные providers, routes | `app` | -| Композиция маршрута и поведение, нужное только этому экрану | `pages/` | -| Самостоятельная пользовательская возможность с собственным смыслом | `features/` | -| Представление/поведение бизнес-сущности | `entities/` | -| Переиспользуемый код без WealthLedger domain meaning | `shared` | - -Локальный FSD profile: `app → pages → features → entities → shared`, без `widgets`. -Крупный уникальный блок остаётся в своей page. Form, mutation или click handler -сами по себе не требуют feature. Два callers не делают domain helper generic shared. -Slice именуется по ответственности, а не `helpers`, `hooks` или `types`. - -**Безопасный путь:** при сомнении оставь код у ближайшего допустимого owner. -Извлекай его, когда появилась самостоятельная ответственность или реальный reuse. -Не вводи новый layer и не обходи public API, чтобы устранить неудобный import. - -## Imports и public API - -Cross-slice consumer использует `@/` и `index.ts` владельца, например -`@/entities/portfolio`. Alias Vite и TypeScript согласован. Другой slice не -импортирует внутренний файл напрямую. Slices `pages`, `features`, `entities` не зависят от соседнего slice своего слоя; -композиция принадлежит вышележащему owner. Existing меж-slice исключения -не являются разрешением добавлять новые. - -Направление зависимостей и public imports частично проверяет -`npm run deps`; Knip проверяет использование exports, а не смысл placement. - -## UI foundation - -Mental model, theme и data flow — в [architecture](../architecture.md). -При изменении foundation прочитай [design system](../../../docs/design/overview.md). - -## Примеры - -- Общая busy-область: существующий [LoadingSkeleton](../../src/shared/ui/LoadingSkeleton.tsx), а не domain widget в shared. -- Account-income state views: [страница](../../src/pages/account-income/ui/AccountIncomePage.tsx) и её flat `components/*.view.tsx`; новый page-local filter остаётся здесь. -- Portfolio query: [entity public API](../../src/entities/portfolio/index.ts); новая UI-сводка с бизнес-смыслом не становится shared только из-за нескольких экранов. - -## Проверка - -После placement/import change: `npm run deps`, `npm run typecheck`, `npm run knip`; -после собственного behavior — применимые tests. Финальный gate указан в AGENTS. -Existing dependency config не запрещает все same-layer imports: их и смысл -owner проверяет review. Не объявляй Steiger установленным; graph проверяет -текущий dependency-cruiser. Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает -правильный выбор слоя. diff --git a/.ai/conventions/storybook.md b/.ai/conventions/storybook.md index c82e9d0..62fb6b9 100644 --- a/.ai/conventions/storybook.md +++ b/.ai/conventions/storybook.md @@ -29,5 +29,5 @@ RTL suite и не тестируй внутренности Mantine. Сущес `npm run typecheck`, `npm run deps:storybook`, `npm run build:catalog` проверяют типы, изоляцию и сборку. Build не выполняет `play` как CI interaction suite; для проверки interaction открой story в браузере, для regression используй -существующий [Testing](frontend-testing.md). Визуальное соответствие задаче, +существующий [Testing](testing.md). Визуальное соответствие задаче, keyboard и узкий viewport проверяются отдельно от успешной сборки. diff --git a/.ai/conventions/frontend-testing.md b/.ai/conventions/testing.md similarity index 100% rename from .ai/conventions/frontend-testing.md rename to .ai/conventions/testing.md diff --git a/.ai/conventions/typescript-react.md b/.ai/conventions/typescript-react.md index fee0393..4dd839c 100644 --- a/.ai/conventions/typescript-react.md +++ b/.ai/conventions/typescript-react.md @@ -27,7 +27,7 @@ Hooks вызываются без условий и ранних выходов контракту стабильной identity; не добавляй их ко всем значениям/handlers. React Compiler включён в Vite, но не доказывает выигрыш и не заменяет effect correctness. Не удаляй существующую memoization без понимания её consumers. -Container/VM/view и state owners — в [UI](frontend-ui.md). +Container/VM/view и state owners — в [UI](ui.md). ## Domain literals and magic values @@ -39,15 +39,15 @@ Backend vocabulary бери из generated `paths`, не создавай руч `ProblemDetailsDto.code` сейчас `string`. Не объявляй закрытый frontend error enum и не превращай неизвестный code в известный через cast. Повторяющуюся -обработку своди у ответственного owner; [errors](frontend-data.md#errors). +обработку своди у ответственного owner; [errors](data.md#errors). `noMagicNumbers` в Biome — error; fixtures/tests/stories исключены только из этого правила. Policy получает имя и единицы; подавление допустимо для incidental value с объяснением. Обычный UI-текст не превращай в constant: -его owner — [i18n](frontend-ui.md#строки-и-валидация). +его owner — [i18n](ui.md#строки-и-валидация). Semantic magic strings и props profile требуют review. ## Проверка `npm run typecheck` и `npm run lint`; behavior проверяй по -[Testing](frontend-testing.md), placement — по [FSD](frontend-architecture.md). +[Testing](testing.md), placement — по [FSD](../architecture.md). diff --git a/.ai/conventions/frontend-ui.md b/.ai/conventions/ui.md similarity index 98% rename from .ai/conventions/frontend-ui.md rename to .ai/conventions/ui.md index f1c13f4..1f98622 100644 --- a/.ai/conventions/frontend-ui.md +++ b/.ai/conventions/ui.md @@ -22,7 +22,7 @@ Wrapper нужен для проектной ответственности, а | Состояние | Владелец | | --- | --- | -| Ответ сервера / query lifecycle | TanStack Query, [data rules](frontend-data.md) | +| Ответ сервера / query lifecycle | TanStack Query, [data rules](data.md) | | Ввод и ошибки формы | React Hook Form + Zod, container/VM | | Параметр навигации, который должен переживать переход/ссылку | Router/URL | | Локальное открытие, focus, rendering fallback | React UI state | @@ -101,6 +101,6 @@ Types, props, hooks и memoization — в [TypeScript/React](typescript-react.md ## Проверка Type/style: `npm run typecheck`, `npm run lint`. Для risk behavior/изменения тестов -используй [Testing](frontend-testing.md). UI: keyboard, narrow viewport, +используй [Testing](testing.md). UI: keyboard, narrow viewport, loading/error/empty/loaded и recovery по применимости. [Storybook](storybook.md) задаёт выбор stories и изоляцию каталога; сверяй UI с задачей отдельно от build. diff --git a/AGENTS.md b/AGENTS.md index 3d9dff9..709069b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,21 +2,20 @@ Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, путей и обнаруженных boundaries; читай только нужные темы до решения. -[Sources](.ai/SOURCES.md) выбирает факты/spec/ADR по возникшему вопросу. +Системные факты/spec/ADR выбирай в разделе [Источники](#источники) по вопросу. | Решение / сигнал | Читать | | --- | --- | -| Первое знакомство, data flow, общая архитектура | [Mental model](.ai/architecture.md) | -| Создать/перенести component/module/slice, owner/FSD, cross-slice imports | [FSD](.ai/conventions/frontend-architecture.md) | +| Устройство/data flow, component/module/slice, owner/FSD, cross-slice imports | [Architecture](.ai/architecture.md) | | TS types/props/imports/exports, hooks/effects, derived state, memoization, domain literals | [TypeScript/React](.ai/conventions/typescript-react.md) | -| UI/Mantine, form, container/VM/view, state owner, loading/error/recovery | [UI](.ai/conventions/frontend-ui.md) | -| Только продуктовая строка / Zod message | [Строки](.ai/conventions/frontend-ui.md#строки-и-валидация) | -| Query/mutation, server cache, shared/api, generated types | [Data](.ai/conventions/frontend-data.md) | -| API/network/application errors, codes и сообщения | [Errors](.ai/conventions/frontend-data.md#errors) | -| Login/refresh/logout, token/cookie, session ownership | Data + [Security](../backend/.ai/conventions/security-auth.md) + [Testing](.ai/conventions/frontend-testing.md) | +| UI/Mantine, form, container/VM/view, state owner, loading/error/recovery | [UI](.ai/conventions/ui.md) | +| Только продуктовая строка / Zod message | [Строки](.ai/conventions/ui.md#строки-и-валидация) | +| Query/mutation, server cache, shared/api, generated types | [Data](.ai/conventions/data.md) | +| API/network/application errors, codes и сообщения | [Errors](.ai/conventions/data.md#errors) | +| Login/refresh/logout, token/cookie, session ownership | Data + [Security](../backend/.ai/conventions/security-auth.md) + [Testing](.ai/conventions/testing.md) | | Request/response field, route/status/error shape, OpenAPI | Data + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | -| Tests или behavior с regression risk | [Testing](.ai/conventions/frontend-testing.md) | -| Storybook/stories/play, theme/tokens/shared UI | [Storybook](.ai/conventions/storybook.md), UI; design facts — через Sources | +| Tests или behavior с regression risk | [Testing](.ai/conventions/testing.md) | +| Storybook/stories/play, theme/tokens/shared UI | [Storybook](.ai/conventions/storybook.md), UI; design facts — в разделе Источники | | Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | | Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | @@ -24,3 +23,17 @@ Testing и без production diff. Повтори routing по итоговому diff, включая новые файлы. Migration backlog читается только при работе над соответствующим finding через [plan](../.ai/migrations/conventions-v1/plan.md). + +## Источники + +Открывай только источник по возникшему вопросу; примеры кода не переопределяют +контракт. Specs задают behavior, а editable OpenAPI принадлежит backend. + +| Вопрос | Источник | +| --- | --- | +| HTTP contract, validation/error codes | [HTTP API](../docs/architecture/http-api.md) | +| Auth/session/refresh, подключение брокера | [Authentication](../docs/domains/authentication/overview.md), [T-Bank](../docs/integrations/tbank.md) | +| Money, account/position/income/cache meanings | [Глоссарий](../docs/glossary.md) | +| Theme/tokens, UI-каталог | [Дизайн-система](../docs/design/overview.md), [Storybook](README.md#ui-каталог-storybook) | +| Поведение области / требования | [Области docs](../docs/README.md#предметные-области), [Specs index](../specs/AI_INDEX.md) | +| Versions/scripts/check scope | Manifest/lockfile, Biome/TypeScript/dependency configs | diff --git a/README.md b/README.md index ae92092..661e561 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ document и затем выполните `npm run openapi:generate`. ## Документация -- [Router conventions](.ai/CONVENTIONS.md) и [источники](.ai/SOURCES.md) -- [Выбор owner/FSD](.ai/conventions/frontend-architecture.md) -- [UI, state, формы и компоненты](.ai/conventions/frontend-ui.md) -- [Data/API/cache](.ai/conventions/frontend-data.md) -- [Testing](.ai/conventions/frontend-testing.md) +- [AGENTS: правила и источники](AGENTS.md) +- [Выбор owner/FSD](.ai/architecture.md) +- [UI, state, формы и компоненты](.ai/conventions/ui.md) +- [Data/API/cache](.ai/conventions/data.md) +- [Testing](.ai/conventions/testing.md) - [UI-каталог Storybook](#ui-каталог-storybook) - [Обзор поставленной системы](../docs/overview.md) - [Карта проектных фактов](../docs/README.md) -- 2.54.0 From d9ada2762169b3347a0dc4b7e7ce6c7cd207af76 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 20:45:37 +0300 Subject: [PATCH 3/5] docs: make frontend instructions self-contained --- .ai/architecture.md | 5 +- .ai/conventions/data.md | 21 +++++--- .ai/conventions/engineering.md | 76 +++++++++++++++++++++++++++++ .ai/conventions/testing.md | 4 +- .ai/conventions/typescript-react.md | 2 +- .ai/conventions/ui.md | 12 ++++- AGENTS.md | 36 +++++++------- README.md | 53 ++++++++++++++++---- 8 files changed, 165 insertions(+), 44 deletions(-) create mode 100644 .ai/conventions/engineering.md diff --git a/.ai/architecture.md b/.ai/architecture.md index 1eb45fa..877b55d 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -62,9 +62,8 @@ public API `shared/theme`. Storybook показывает synthetic presentation не использует application API. RTL/MSW проверяют поведение приложения отдельно. Existing route-aware AppShell расположен в shared/ui; это legacy placement, -а не образец нового shared primitive. Общий план улучшений читается только -при migration через [router](../AGENTS.md). -При изменении foundation прочитай [design system](../../docs/design/overview.md). +а не образец нового shared primitive. При изменении foundation прочитай +[UI](conventions/ui.md#визуальная-основа) и [Storybook](conventions/storybook.md). ## Примеры diff --git a/.ai/conventions/data.md b/.ai/conventions/data.md index b73d106..d50f3ce 100644 --- a/.ai/conventions/data.md +++ b/.ai/conventions/data.md @@ -28,7 +28,7 @@ client, runtime schema, hash/checksum registry или дополнительны без отдельной текущей потребности; existing OpenAPI flow уже владеет генерацией. Frontend-команды не читают соседний backend checkout и не проверяют backend runtime OpenAPI; изменение wire shape/status/route сохраняет -[границу HTTP-контракта](../../../docs/architecture/http-api.md#источник-контракта). +публичный контракт предоставленного snapshot. Одна UI-модель не требует менять API. `openapi:generate` работает offline от snapshot; `openapi:check` генерирует во временный каталог и сравнивает, не меняя tracked files. @@ -40,18 +40,23 @@ Authorization. Auth endpoints исключены из Bearer/retry middleware. R Не добавляй собственный refresh/retry loop в caller. Protected queries используют existing session-aware boundary; logout очищает -session-scoped cache. При auth/token change читай [security](../../../backend/.ai/conventions/security-auth.md) -и [authentication](../../../docs/domains/authentication/overview.md). +session-scoped cache. Access token хранится только в памяти, refresh передаётся +httpOnly cookie с `credentials: include`. Не сохраняй tokens в localStorage, +URL, логах или fixtures. Logout очищает локальную session/cache и при ошибке +сети; до окончания auth bootstrap показывай neutral state. Перед auth change +прочитай shared client, session owner и их tests. Успешная connection mutation коммитит подтверждённый status и сбрасывает financial GET subtree; ошибка mutation не меняет generation/cache. Это отдельная граница -от обычного фонового refetch: [connection policy](../../../docs/integrations/tbank.md#граница-финансового-кэша). +от обычного фонового refetch. Даже повторное сохранение того же token начинает +новую generation; поздний ответ прежней generation не попадает в новый кэш. Mutations используют существующий generated client и query-key owner; после успеха обновляй/invalidируй только связанные данные. Optimistic update нужен конкретному UX и обязан иметь rollback/error behavior. -Default focus refetch выключен; freshness и retry определяются существующей -[query policy](../../../docs/adr/007-react-query-fetch-policy.md) и feature source. +Default focus refetch выключен; freshness и retry задаются существующими +query options у entity/feature owner. После `staleTime` повторное открытие +экрана может запустить background refetch; focus сам по себе его не запускает. При обычном refetch сохраняй Client Cached Snapshot, при session/connection смене не показывай данные прежнего владельца. UI presentation — в UI convention. @@ -76,5 +81,5 @@ Network/unknown error получает безопасное локальное Изменяя header/retry/invalidation, проверяй наблюдаемое поведение через MSW и [Testing](testing.md); не обращайся к реальному T-Bank. `npm run openapi:check`, `npm run typecheck` и targeted tests проверяют разные -части решения. Для API semantics нужны exact docs и проверки наблюдаемого -HTTP-поведения на стороне backend. +части решения. Для поведения API-клиента используй сценарии MSW на публичной +HTTP-границе; реализация сервера не является входом frontend-проверок. diff --git a/.ai/conventions/engineering.md b/.ai/conventions/engineering.md new file mode 100644 index 0000000..89db9f3 --- /dev/null +++ b/.ai/conventions/engineering.md @@ -0,0 +1,76 @@ +# Работа в репозитории + +## Граница и порядок работы + +Репозиторий сам содержит инструкции, исходники, конфигурацию и входы своих +проверок. Сборка, CI и обязательный маршрут чтения работают без соседних +checkout. Внешний API задаётся предоставленным публичным контрактом; +исходники или инструкции его реализации не являются зависимостью разработки. +При нехватке требования сообщи, какое решение не определено, до его реализации. + +Начни с результата и affected areas. До edits проследи flow/invariants, найди +owner и 1–3 примера. Код показывает механизм; случайный аналог не меняет контракт. +Работай напрямую; OpenSpec/SDD lifecycle — только по явному запросу. +Перед Node-командами активируй местную `.nvmrc`: `nvm use --silent`. +Сохраняй чужие staged/unstaged/untracked изменения и read-only архивы. + +## Change scope + +Сначала расширь существующего владельца. Иначе используй standard library, +платформу или установленную dependency; небольшая новая реализация допустима +при собственной ответственности. Не создавай helper/interface/wrapper ради +возможного reuse. Общее имя не означает общую бизнес-ответственность. +Сохраняй validation, authorization, atomicity, accessibility и migration safety. + +После diff выполни Boy Scout review: проверь новое дублирование, owner, callers, +небольшие связанные legacy issues и соответствие local conventions. Исправляй +непосредственно связанные low-risk проблемы; широкий cleanup — отдельная задача. + +## Domain values + +Finite business vocabulary принадлежит одному domain/module/entity owner. +Переиспользуй его symbols; для нового словаря без аналога — `as const` + union. +Сохраняй wire values; generated/external enums не переписывай вручную. +Numeric policy называй по смыслу и единицам. Индексы, `Math.max(value, 0)` и +обычные labels не выноси в constants без semantic benefit. +Локальный TypeScript/lint профиль определяет дополнительные ограничения. + +## Dependencies + +Проверь аналоги и direct dependencies в местном manifest до добавления пакета. +Новая dependency оправдана текущей потребностью и меньшей общей сложностью; +оцени compatibility, runtime/bundle и maintenance/security. Импортируемый пакет +объявляется напрямую; транзитивная установка не является контрактом. +Сохраняй exact versions и lockfile; воспроизводимая установка — `npm ci`. +Peer-конфликт решай совместимыми версиями без `--force`/`--legacy-peer-deps`. +Используй существующий analyzer; не добавляй второй для той же проверки. + +## Test Intent + +Перед нетривиальным тестом кратко определи наблюдаемый исход, regression risk, +значимые edge/error/concurrency cases и самую дешёвую надёжную boundary. +Отдельный artifact не нужен. Проверяй результат, а не private call order, +декларации, тривиальные wrappers или поведение framework. +При bug сначала воспроизведи исход. Mock race не доказывает DB atomicity. +Уровни и runnable commands выбирай по местной testing convention. + +## Проверки и завершение + +Code/dependency change: `npm ci`, полный `npm run verify` и дополнительные +risk checks из местной testing convention. Targeted tests ускоряют итерацию, +но не заменяют финальный gate. `lint:fix`/format — edits, не proof. +Markdown-only: проверь новые/изменённые тексты, локальные ссылки/якоря, маршруты +чтения и `git diff --check`; application build ради текста не нужен. +Повтори routing по staged/unstaged/new files. Запиши краткие команды, результаты +и ограничения. Зелёный lint не заменяет semantic/security review. + +## Git и релизы + +Работа — в `codex/` от актуального `develop`, затем PR в `develop`. +Сохраняй чужие изменения; перед commit просмотри staged paths и diff. +Commits и PR titles — Conventional Commits; допустим `spec:`. Каждый commit +содержит одну связанную задачу. При связанной issue добавь её номер в конец +заголовка (`fix: … #12`); без issue номер не нужен. Root другого checkout +не управляет этим repo. +Прямой push в `main`, force-push и перенос опубликованных тегов не входят в +обычную работу. Перед release/hotfix прочитай местный README, раздел «Релизы». diff --git a/.ai/conventions/testing.md b/.ai/conventions/testing.md index 8b3513a..740656a 100644 --- a/.ai/conventions/testing.md +++ b/.ai/conventions/testing.md @@ -1,7 +1,7 @@ # Frontend testing Перед нетривиальными тестами сформулируй -[общий Test Intent](../../../.ai/conventions/testing.md); отдельный документ +[общий Test Intent](engineering.md#test-intent); отдельный документ не требуется. Trivial low-risk изменения обходятся без него. ## Выбор проверки @@ -35,7 +35,7 @@ MSW запускается только Vitest setup в `src/test/`, без brow экземпляр и seeded cache. Providers выбирай явно. Existing teardown выполняет RTL cleanup, auth reset и MSW reset: [test client](../../src/test/query.tsx). Тестовый клиент отключает retries запросов и мутаций для предсказуемого исхода; -это не меняет [политику обновления приложения](../../../docs/adr/007-react-query-fetch-policy.md). +это не меняет [политику обновления приложения](data.md#auth-и-invalidation). ## Важные случаи diff --git a/.ai/conventions/typescript-react.md b/.ai/conventions/typescript-react.md index 4dd839c..462ce0c 100644 --- a/.ai/conventions/typescript-react.md +++ b/.ai/conventions/typescript-react.md @@ -32,7 +32,7 @@ Container/VM/view и state owners — в [UI](ui.md). ## Domain literals and magic values Общее правило ownership и символических значений — -[Domain values](../../../.ai/conventions/code-health.md#domain-values). +[Domain values](engineering.md#domain-values). Backend vocabulary бери из generated `paths`, не создавай ручную копию enum. Текущий `schema.d.ts` содержит только types: typed wire literals допустимы, когда generator не предоставляет runtime values. diff --git a/.ai/conventions/ui.md b/.ai/conventions/ui.md index 1f98622..badd161 100644 --- a/.ai/conventions/ui.md +++ b/.ai/conventions/ui.md @@ -18,7 +18,7 @@ components живут у feature/entity/page, даже при повторном Wrapper нужен для проектной ответственности, а не ради нового имени для Button. Локальный CSS module принадлежит component; общий token вводи для -реально общей роли через [design system](../../../docs/design/overview.md). +реально общей роли в [визуальной основе](#визуальная-основа). | Состояние | Владелец | | --- | --- | @@ -104,3 +104,13 @@ Type/style: `npm run typecheck`, `npm run lint`. Для risk behavior/измен используй [Testing](testing.md). UI: keyboard, narrow viewport, loading/error/empty/loaded и recovery по применимости. [Storybook](storybook.md) задаёт выбор stories и изоляцию каталога; сверяй UI с задачей отдельно от build. + +## Визуальная основа + +Общая тема живёт в `src/shared/theme`: `tokens.json` задаёт переносимые значения, +`theme.ts` адаптирует их к Mantine. Используй public API темы и её semantic roles, +без отдельной палитры в page/component. Поддерживается светлая схема. +Цвет дополняет подпись или знак; проверяй контраст конкретной пары text/background. +Warning не используй для мелкого текста на белом. Деньги сохраняют валюту, знак +и различие между нулём и неизвестной суммой. Общие состояния демонстрирует +Storybook; presentation stories используют synthetic data без application API. diff --git a/AGENTS.md b/AGENTS.md index 709069b..7936743 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,9 @@ # Frontend router -Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, -путей и обнаруженных boundaries; читай только нужные темы до решения. -Системные факты/spec/ADR выбирай в разделе [Источники](#источники) по вопросу. +Репозиторий самостоятельный: обязательные инструкции и проверки используют +только его файлы и предоставленные публичные контракты. Перед работой прочитай +[порядок работы](.ai/conventions/engineering.md#граница-и-порядок-работы), затем +объединяй triggers задачи и путей; читай активированные темы до решения. | Решение / сигнал | Читать | | --- | --- | @@ -12,28 +13,25 @@ | Только продуктовая строка / Zod message | [Строки](.ai/conventions/ui.md#строки-и-валидация) | | Query/mutation, server cache, shared/api, generated types | [Data](.ai/conventions/data.md) | | API/network/application errors, codes и сообщения | [Errors](.ai/conventions/data.md#errors) | -| Login/refresh/logout, token/cookie, session ownership | Data + [Security](../backend/.ai/conventions/security-auth.md) + [Testing](.ai/conventions/testing.md) | -| Request/response field, route/status/error shape, OpenAPI | Data + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | +| Login/refresh/logout, token/cookie, session ownership | [Data/auth](.ai/conventions/data.md#auth-и-invalidation) + [Testing](.ai/conventions/testing.md) | +| Request/response field, route/status/error shape, OpenAPI | [Data/contract](.ai/conventions/data.md#типы-и-contract) | | Tests или behavior с regression risk | [Testing](.ai/conventions/testing.md) | | Storybook/stories/play, theme/tokens/shared UI | [Storybook](.ai/conventions/storybook.md), UI; design facts — в разделе Источники | -| Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | -| Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | +| Helper/dependency/новый pattern, refactor/scope | [Change scope](.ai/conventions/engineering.md#change-scope), для пакета — [Dependencies](.ai/conventions/engineering.md#dependencies) | +| Выбор gate, tooling, завершение | [Проверки](.ai/conventions/engineering.md#проверки-и-завершение) | Показ query state не означает изменение fetching. Изменение тестов активирует -Testing и без production diff. Повтори routing по итоговому diff, включая новые -файлы. Migration backlog читается только при работе над соответствующим finding -через [plan](../.ai/migrations/conventions-v1/plan.md). +Testing и без production diff. Перед завершением повтори routing по новым и +изменённым файлам. Для Git/release прочитай +[Git и релизы](.ai/conventions/engineering.md#git-и-релизы). ## Источники -Открывай только источник по возникшему вопросу; примеры кода не переопределяют -контракт. Specs задают behavior, а editable OpenAPI принадлежит backend. - -| Вопрос | Источник | +| Вопрос | Локальный источник | | --- | --- | -| HTTP contract, validation/error codes | [HTTP API](../docs/architecture/http-api.md) | -| Auth/session/refresh, подключение брокера | [Authentication](../docs/domains/authentication/overview.md), [T-Bank](../docs/integrations/tbank.md) | -| Money, account/position/income/cache meanings | [Глоссарий](../docs/glossary.md) | -| Theme/tokens, UI-каталог | [Дизайн-система](../docs/design/overview.md), [Storybook](README.md#ui-каталог-storybook) | -| Поведение области / требования | [Области docs](../docs/README.md#предметные-области), [Specs index](../specs/AI_INDEX.md) | +| HTTP request/response/errors | [OpenAPI snapshot](src/shared/api/spec/openapi.json), generated types и [Data](.ai/conventions/data.md) | +| Auth/session/refresh/cache | [Data](.ai/conventions/data.md#auth-и-invalidation), source и colocated tests владельца | +| Money, account/position/income | Entity types, API snapshot, formatters и tests соответствующего slice | +| Theme/tokens, UI-каталог | [UI](.ai/conventions/ui.md), [Storybook](README.md#ui-каталог-storybook) | +| Поведение области | Source и tests её page/entity/feature; неясное ожидание уточни до изменения | | Versions/scripts/check scope | Manifest/lockfile, Biome/TypeScript/dependency configs | diff --git a/README.md b/README.md index 661e561..805f09d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ document и затем выполните `npm run openapi:generate`. `npm run build:catalog`. Каталог использует общую тему приложения, вымышленные данные и локальные действия без application API. Примеры не доказывают восстановление реального запроса. Визуальные роли описаны в -[дизайн-системе](../docs/design/overview.md). +[UI convention](.ai/conventions/ui.md#визуальная-основа). ## Документация @@ -65,16 +65,49 @@ document и затем выполните `npm run openapi:generate`. - [Data/API/cache](.ai/conventions/data.md) - [Testing](.ai/conventions/testing.md) - [UI-каталог Storybook](#ui-каталог-storybook) -- [Обзор поставленной системы](../docs/overview.md) -- [Карта проектных фактов](../docs/README.md) -Runtime OpenAPI backend сверяется с frontend snapshot отдельной read-only -backend командой `npm run test:openapi-contract`. -Разработка идёт напрямую по задаче пользователя. Каждый affected repo проходит -свой verify; при изменении wire contract дополнительно выполняется contract check. -Обязательного OpenSpec/SDD lifecycle нет. - -Версии и ветки: [процесс релиза](../README.md#релизы-и-ветки). +Checkout frontend содержит все входы сборки и проверок. Публичная схема API +поступает как отдельный artifact; команды frontend используют только её +локальную копию и не обращаются к исходникам или CI сервера. Доставка новой +схемы — явное обновление snapshot с review и `npm run openapi:generate`. +Runtime URL API задаётся окружением, а не расположением другого репозитория. +Разработка идёт напрямую по задаче пользователя; OpenSpec/SDD — по явному запросу. Knip исключает `conventional-changelog-conventionalcommits`: release-it загружает preset по имени из `.release-it.json`. Версия preset закреплена напрямую. + +## Релизы + +Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые +релизы. Работа: `codex/` от актуального `develop`, PR в `develop`. +Обычные PR допускают squash; release и обратная синхронизация сохраняют историю. +Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай. + +1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что + `develop` содержит последний релиз. Создай `release/` от проверенного + `develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в + `release/hotfix-` и проходит тот же процесс. +2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog, + затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`, + обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи + guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии. +3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку, + открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных + `verify`, `security`, `commits` выполни fast-forward-only merge. Если `main` + изменился, согласуй историю и повтори проверки кандидата. +4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией + manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA: + `git tag -a vX.Y.Z -m "Release X.Y.Z"`, затем отправь только + `git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`. +5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь, + что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после + merge создай отсутствующий тег без нового bump; до merge исправь и проверь + существующий кандидат без повторного повышения версии. + +`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch; +`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):` +исключён из рекомендации и changelog. Конфигурация — `.release-it.json`. +В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход +администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены. +Workflow сам не включает эти настройки. Отдельное review approval для одного +владельца не требуется. Npm publishing, deploy и Gitea Releases не включены. -- 2.54.0 From e31ab35a5ac51b7dbd1509e35c4ce8e9f8fd4e92 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 09:41:38 +0300 Subject: [PATCH 4/5] feat: add frontend container delivery --- .dockerignore | 15 ++++++++++ .gitea/workflows/image.yml | 61 ++++++++++++++++++++++++++++++++++++++ Dockerfile | 30 +++++++++++++++++++ README.md | 25 ++++++++++++++++ docker/nginx.conf | 43 +++++++++++++++++++++++++++ vite.config.ts | 7 +++++ 6 files changed, 181 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/image.yml create mode 100644 Dockerfile create mode 100644 docker/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ec510c1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +** +!package.json +!package-lock.json +!.nvmrc +!index.html +!tsconfig*.json +!vite.config.ts +!.storybook/ +!.storybook/** +!src/ +!src/** +!public/ +!public/** +!docker/ +!docker/** diff --git a/.gitea/workflows/image.yml b/.gitea/workflows/image.yml new file mode 100644 index 0000000..4e48340 --- /dev/null +++ b/.gitea/workflows/image.yml @@ -0,0 +1,61 @@ +name: Frontend image + +on: + push: + tags: ['v*'] + +concurrency: + group: frontend-image-publication + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + env: + IMAGE: git.personal.ksv741.ru/wealthledger/frontend + RELEASE_TAG: ${{ gitea.ref_name }} + REVISION: ${{ gitea.sha }} + DOCKER_CONFIG: ${{ runner.temp }}/wealthledger-frontend-docker + steps: + - uses: https://github.com/actions/checkout@v4 + - uses: https://github.com/actions/setup-node@v4 + with: + node-version-file: .nvmrc + - name: Validate release version + run: | + node --input-type=commonjs -e 'const p = require("./package.json"); const l = require("./package-lock.json"); if (!/^v\d+\.\d+\.\d+$/.test(process.env.RELEASE_TAG) || process.env.RELEASE_TAG !== `v${p.version}` || l.version !== p.version || l.packages[""].version !== p.version) process.exit(1)' + test "$(git rev-parse HEAD)" = "$REVISION" + docker version + docker buildx version + - run: npm ci + env: + HUSKY: '0' + - run: npm run verify + env: + KNIP_DISABLE_RAW_TRANSFER: '1' + - name: Login with dedicated package writer + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + test -n "$REGISTRY_USERNAME" && test -n "$REGISTRY_TOKEN" + install -d -m 700 "$DOCKER_CONFIG" + printf '%s' "$REGISTRY_TOKEN" | docker login git.personal.ksv741.ru --username "$REGISTRY_USERNAME" --password-stdin + - name: Publish immutable release tag + run: | + VERSION="${RELEASE_TAG#v}" + if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then + echo 'Refusing to overwrite an existing release tag'; exit 1 + fi + if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then + echo 'Cannot prove tag absence; refusing publication'; exit 1 + fi + docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . + DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" + docker pull "$IMAGE@$DIGEST" + test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION" + docker run --rm --entrypoint sh "$IMAGE@$DIGEST" -c 'test -s /usr/share/nginx/html/index.html && nginx -t && nginx && wget -q -O /dev/null http://127.0.0.1:8080/' + printf 'Frontend image: %s@%s\n' "$IMAGE" "$DIGEST" + - name: Remove registry session + if: always() + run: rm -rf "$DOCKER_CONFIG" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..88feb2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:24.21.0-bookworm-slim AS dependencies +WORKDIR /app +ENV HUSKY=0 +COPY package.json package-lock.json ./ +RUN npm ci + +FROM dependencies AS development +COPY . . +ENV VITE_API_BASE_URL="" API_PROXY_TARGET=http://backend:3000 +EXPOSE 8080 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8080", "--strictPort"] + +FROM dependencies AS build +COPY . . +ENV VITE_API_BASE_URL="" +RUN npm run build + +FROM nginx:1.28.0-alpine AS production +ARG VERSION +ARG REVISION +LABEL org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.revision=$REVISION +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY --from=build /app/dist /usr/share/nginx/html +USER nginx +EXPOSE 8080 +ENTRYPOINT ["nginx"] +CMD ["-g", "daemon off;"] +HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=6 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1 diff --git a/README.md b/README.md index 805f09d..d5f4d81 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,31 @@ staged diff с полной редакцией секретов, затем за Находка или отсутствие Gitleaks блокирует commit. Проверка этого поведения: `node --test scripts/test-pre-commit.mjs`. +## Контейнерный image + +`Dockerfile` имеет targets `development` (Vite HMR и proxy `/api/` во внутренний backend) +и `production` (nginx на 8080, SPA fallback и same-origin API proxy). Node соответствует `.nvmrc`. +Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules +не попадают в image. + +```bash +docker build --target development -t wealthledger-frontend:development . +docker build --target production -t wealthledger-frontend:production . +``` + +Gitea `image.yml` запускается на защищённом release tag `vX.Y.Z`, проверяет +package/lock version и canonical gates. Runner `ubuntu-latest` должен иметь +Docker/buildx. Dedicated package writer задаётся Actions secrets +`REGISTRY_USERNAME`/`REGISTRY_TOKEN`; server использует отдельную read-only +identity. Публикуются semver tags, без `latest`; существующие +release tags не перезаписываются. Production выбирает опубликованный version tag. + +Compose topology принадлежит workspace, эксплуатационная последовательность — +[документация](https://git.personal.ksv741.ru/WealthLedger/docs), +страница `architecture/deployment.md`. +Проверка публикации требует настроенного registry/runner; локальный build +не доказывает package permissions. + ## Проверка ```bash diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..ef37b03 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,43 @@ +worker_processes auto; +pid /tmp/nginx.pid; +error_log /dev/stderr warn; +events { worker_connections 1024; } +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + # Do not log query strings, cookies, authorization or bodies. + log_format safe '$request_method $uri $status'; + access_log /dev/stdout safe; + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + map $http_x_forwarded_proto $forwarded_proto { + default $http_x_forwarded_proto; + '' $scheme; + } + server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + resolver 127.0.0.11 valid=5s ipv6=off; + location /api/ { + set $backend http://backend:3000; + proxy_pass $backend$request_uri; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Proto $forwarded_proto; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Request-ID $http_x_request_id; + } + location /assets/ { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache" always; + } + } +} diff --git a/vite.config.ts b/vite.config.ts index 4c8a6bc..a994fd2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,4 +7,11 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react(), babel({ presets: [reactCompilerPreset()] })], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, + server: { + proxy: { + "/api/": { + target: process.env.API_PROXY_TARGET ?? "http://localhost:3000", + }, + }, + }, }); -- 2.54.0 From 97f1ad3e89f2df089f02cd49ccb17ab97309a106 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 16 Sep 2026 11:35:44 +0300 Subject: [PATCH 5/5] chore(release): 0.2.0 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 247d314..3c6ca60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.2.0](https://git.personal.ksv741.ru/WealthLedger/frontend/compare/v0.1.0...v0.2.0) (2026-09-16) + +### Features + +* add frontend container delivery ([e31ab35](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/e31ab35a5ac51b7dbd1509e35c4ce8e9f8fd4e92)) + +### Documentation + +* make frontend instructions self-contained ([d9ada27](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/d9ada2762169b3347a0dc4b7e7ce6c7cd207af76)) +* refine frontend conventions and routing ([718365c](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/718365cd5ab51e7c696c777b71095f7ee41b471d)) +* simplify conventions layout ([9f7e342](https://git.personal.ksv741.ru/WealthLedger/frontend/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3)) + ## 0.1.0 (2026-09-14) ### Features diff --git a/package-lock.json b/package-lock.json index a76aef2..297c33a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wealth-ledger-frontend", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wealth-ledger-frontend", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@hookform/resolvers": "5.9.1", "@mantine/charts": "9.6.1", diff --git a/package.json b/package.json index 81ea85b..c5f7caf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wealth-ledger-frontend", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "engines": { "node": "24.21.0" -- 2.54.0