added AGENTS.md and docs in root dir
Android CI / Build and Test (push) Failing after 13m25s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

This commit is contained in:
wt
2026-09-08 13:36:43 +07:00
parent 2f891c9503
commit da9e17f45f
7 changed files with 728 additions and 7 deletions
+167
View File
@@ -0,0 +1,167 @@
# AGENTS.md — корень монорепозитория Balance
Этот файл — точка входа для AI-агентов и разработчиков, работающих в
репозитории Balance. Он описывает, как устроены три подпроекта, как они
связаны друг с другом, и куда идти за подробностями. **Каждый подпроект
имеет собственный `AGENTS.md` с детальными правилами — читай его перед
любым изменением внутри соответствующей папки.**
## Что это за проект
Balance — приложение для учёта личных финансов, доступное на пяти
платформах, которые используют одну и ту же доменную модель и один и
тот же протокол синхронизации:
| Платформа | Папка | Стек |
| --- | --- | --- |
| iOS 17+ | `Balance/` | SwiftUI + SwiftData |
| macOS 14+ | `Balance/` (`BalanceMac/`) | SwiftUI + SwiftData |
| watchOS 10+ | `Balance/` (`BalanceWatch/`) | SwiftUI + SwiftData |
| Android 8.0+ | `BalanceAndroid/` | Kotlin + Jetpack Compose + Room |
| Web (встроен в сервер) | `BalanceServer/` | Go + embedded static client |
| Сервер синхронизации | `BalanceServer/` | Go + SQLite |
Все клиенты — iOS, macOS, watchOS, Android, web — синхронизируются через
один и тот же self-hosted сервер (`BalanceServer`) по единому HTTP(S)
API: регистрация, вход, refresh-токены, push/pull изменений с курсором,
tombstone-удаления, пользовательские категории и месячные бюджеты.
## Структура репозитория
```text
Balance/ # монорепозиторий
├── AGENTS.md # этот файл — общие правила для агентов
├── README.md # общий обзор и точки входа
├── docs/ # сводная документация по всему проекту
│ ├── ARCHITECTURE.md # как связаны клиенты и сервер
│ ├── SYNC_PROTOCOL.md # единый протокол синхронизации
│ ├── DATA_MODEL.md # сопоставление сущностей между платформами
│ ├── SECURITY.md # сквозные требования безопасности
│ └── DEVELOPMENT.md # как собрать и проверить все три проекта
│
├── Balance/ # Xcode-проект: iOS + macOS + watchOS
│ ├── AGENTS.md # правила для Apple-клиента
│ ├── README.md
│ └── docs/ # ARCHITECTURE, DATA_MODEL, SYNC, DEVELOPMENT, PRODUCT
│
├── BalanceAndroid/ # Android-проект (Gradle/Kotlin)
│ ├── AGENTS.md # правила для Android-клиента
│ ├── README.md
│ └── docs/ # ARCHITECTURE, DATA_MODEL, SYNC, DEVELOPMENT, PRODUCT
│
└── BalanceServer/ # Go-сервер синхронизации + web-клиент
├── AGENTS.md # правила для сервера
├── README.md
└── docs/ # ARCHITECTURE, API, DATA_MODEL, SYNC, SECURITY, DEVELOPMENT, DEPLOYMENT, PRODUCT
```
## Как работать в этом репозитории
### 1. Сначала пойми, в каком подпроекте изменение
Каждый подпроект — независимый билд с собственными зависимостями,
тестами и CI-шагами. Изменения в одной папке не должны требовать
правок в другой, **кроме** случаев, когда меняется общий контракт:
- HTTP API / формат sync-запросов и ответов (`BalanceServer`);
- набор синхронизируемых полей сущности (транзакция, категория, бюджет);
- правила финансовых расчётов (баланс, корректировки, аналитика).
Если меняется общий контракт — обнови все три подпроекта согласованно
и синхронизируй документацию (`docs/SYNC_PROTOCOL.md`, а также
`docs/SYNC.md`/`docs/API.md`/`docs/DATA_MODEL.md` внутри каждой папки).
### 2. Единая доменная модель
Три клиента независимо реализуют одну и ту же модель на своём стеке
(SwiftData / Room), но со семантически одинаковыми сущностями:
- **Транзакция** (доход/расход, сумма, дата, заметка, категория —
хранится snapshot'ом имени/иконки/цвета категории на момент операции);
- **Пользовательская категория** (имя, иконка/emoji, цвет);
- **Месячный бюджет** (категория, месяц, лимит);
- **Tombstone/deletion journal** для распространения удалений между
устройствами.
Общие правила расчётов (баланс = доходы − расходы; корректировки
баланса учитываются в общем балансе, но не в периодической аналитике;
расходы группируются по имени категории) продублированы в:
- `Balance/Balance/Utilities/FinanceCalculations.swift` (Apple);
- `BalanceAndroid/.../data/FinanceMath.kt` (Android).
При изменении этих правил на одной платформе — переноси изменение на
другую и обновляй оба набора тестов
(`BalanceTests/FinanceCalculationsTests.swift`, `FinanceMathTest`).
### 3. Синхронизация — общий контракт
Единый протокол описан в `docs/SYNC_PROTOCOL.md` (сводно) и подробно —
в `BalanceServer/docs/API.md` и `BalanceServer/docs/SYNC.md`. Клиентские
реализации:
- Apple: `Balance/Balance/Shared/ServerSync.swift`;
- Android: `BalanceAndroid/.../sync/SyncRepository.kt`.
Изменение cursor/sequence semantics, JSON-полей или auth-заголовков —
breaking change для всех клиентов сразу. Не меняй его в одном
подпроекте, не проверив остальные два.
### 4. Безопасность (сквозные требования)
- Секреты (пароли, access/refresh токены) никогда не хранятся в
открытом виде: Keychain (Apple), Android Keystore + AES/GCM
(Android), Argon2id-хеши + SHA-256 refresh-token хеши (сервер).
- HTTPS обязателен для удалённого сервера на всех клиентах; HTTP
разрешён только для `localhost`/`127.0.0.1`/`::1`.
- Не логируй пароли, токены или заголовок `Authorization` ни в одном
из подпроектов.
- Не коммить секреты, production-URL или credentials.
Подробности — `docs/SECURITY.md` и `BalanceServer/docs/SECURITY.md`.
### 5. Тестирование перед PR
Запусти проверки для всех подпроектов, которые ты менял:
```bash
# Apple (из Balance/)
xcodebuild -project Balance.xcodeproj -scheme Balance \
-destination 'platform=iOS Simulator,name=<available iPhone>' test
# Android (из BalanceAndroid/)
./gradlew clean test assembleDebug
# Server (из BalanceServer/)
go test ./...
go vet ./...
go build ./...
```
Если менялся общий контракт (API/sync/data model) — прогони проверки
во всех трёх, а не только в изменённой папке.
### 6. Документация
- Меняешь контракт синхронизации → обнови `docs/SYNC_PROTOCOL.md` в
корне **и** `BalanceServer/docs/SYNC.md`/`API.md`, а также
`docs/SYNC.md` в `Balance/` и `BalanceAndroid/`.
- Меняешь модель данных на любой платформе → обнови `docs/DATA_MODEL.md`
в корне и в затронутом подпроекте.
- Меняешь способ сборки/запуска → обнови `docs/DEVELOPMENT.md` в
корне и соответствующий `README.md`/`docs/DEVELOPMENT.md` подпроекта.
- Каждый подпроект содержит собственный подробный `AGENTS.md` — этот
файл не заменяет их, а даёт общую картину и правила координации между
платформами.
## PR checklist (для изменений, затрагивающих несколько платформ)
- [ ] Определены все подпроекты, которые затрагивает изменение.
- [ ] Общий контракт (API/sync/data model) обновлён согласованно во
всех платформах, а не только в одной.
- [ ] Финансовые расчёты остаются идентичными на Apple и Android.
- [ ] Обновлены `docs/SYNC_PROTOCOL.md`, `docs/DATA_MODEL.md` в корне,
если менялась схема/протокол.
- [ ] Секреты и токены не добавлены в репозиторий ни в одном подпроекте.
- [ ] Тесты пройдены в каждом изменённом подпроекте.
- [ ] Локальный `AGENTS.md` соответствующего подпроекта тоже соблюдён.
+79 -7
View File
@@ -1,13 +1,85 @@
# Balance — все платформы # Balance — все платформы
В архиве находятся нативные приложения личных финансов и общий self-hosted сервер синхронизации. Balance — приложение для учёта личных финансов с нативными клиентами
для iOS, macOS, watchOS, Android и встроенным web-интерфейсом, а также
self-hosted сервер синхронизации на Go. В архиве находится
монорепозиторий из трёх независимо собираемых проектов.
| Папка | Назначение | | Папка | Назначение | Стек |
| --- | --- | --- |
| [`Balance/`](Balance/README.md) | iOS 17+, macOS 14+ и watchOS 10+ | SwiftUI + SwiftData |
| [`BalanceAndroid/`](BalanceAndroid/README.md) | Android 8.0+ | Kotlin + Jetpack Compose + Room |
| [`BalanceServer/`](BalanceServer/README.md) | Go-сервер авторизации, синхронизации и встроенный web-клиент | Go + SQLite |
Все пять клиентов — iOS, macOS, watchOS, Android и web — используют
одну модель данных и один протокол сервера: операции, пользовательские
категории, бюджеты, удаления, регистрация, вход, refresh-токены и
постраничная двусторонняя синхронизация. Web-интерфейс открывается по
адресу запущенного сервера и не требует отдельной установки.
## Быстрый старт
1. **Поднять сервер** — см. [`BalanceServer/README.md`](BalanceServer/README.md#quick-start-with-docker)
(Docker или локальная сборка Go-бинарника).
2. **Открыть Apple-проект** — `open Balance/Balance.xcodeproj`, см.
[`Balance/README.md`](Balance/README.md).
3. **Собрать Android-приложение** — `cd BalanceAndroid && ./gradlew assembleDebug`,
см. [`BalanceAndroid/README.md`](BalanceAndroid/README.md).
4. В любом клиенте укажите адрес запущенного сервера в настройках
аккаунта, чтобы включить синхронизацию между устройствами.
## Документация
Документация организована на двух уровнях:
- **Корневой уровень** (`docs/`) — то, что относится ко всем платформам
сразу: как устроена система в целом, единый протокол синхронизации,
сквозная модель данных и требования безопасности.
- **Уровень подпроекта** (`Balance/docs/`, `BalanceAndroid/docs/`,
`BalanceServer/docs/`) — детали конкретной платформы: код, схемы,
сборка, деплой.
| Документ | Описание |
| --- | --- | | --- | --- |
| `Balance` | iOS 17+, macOS 14+ и watchOS 10+, SwiftUI + SwiftData | | [`AGENTS.md`](AGENTS.md) | Правила для AI-агентов и разработчиков, работающих во всём репозитории |
| `BalanceAndroid` | Android 8.0+, Kotlin + Jetpack Compose + Room | | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Как связаны клиенты и сервер, общая схема системы |
| `BalanceServer` | Go-сервер авторизации и синхронизации со встроенным web-приложением | | [`docs/SYNC_PROTOCOL.md`](docs/SYNC_PROTOCOL.md) | Единый протокол синхронизации между всеми клиентами и сервером |
| [`docs/DATA_MODEL.md`](docs/DATA_MODEL.md) | Сопоставление доменных сущностей между платформами |
| [`docs/SECURITY.md`](docs/SECURITY.md) | Сквозные требования безопасности (токены, HTTPS, хранение секретов) |
| [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | Как собрать, запустить и протестировать все три проекта |
Все пять клиентов — iOS, macOS, watchOS, Android и web — используют одну модель данных и один протокол сервера: операции, пользовательские категории, бюджеты, удаления, регистрация, вход, refresh-токены и постраничная двусторонняя синхронизация. Web-интерфейс открывается по адресу запущенного сервера и не требует отдельной установки. ### Документация по подпроектам
Инструкции по запуску находятся в `Balance/README.md`, `BalanceAndroid/README.md` и `BalanceServer/README.md`. - **Apple (`Balance/`):** [`AGENTS.md`](Balance/AGENTS.md) ·
[`docs/ARCHITECTURE.md`](Balance/docs/ARCHITECTURE.md) ·
[`docs/DATA_MODEL.md`](Balance/docs/DATA_MODEL.md) ·
[`docs/SYNC.md`](Balance/docs/SYNC.md) ·
[`docs/DEVELOPMENT.md`](Balance/docs/DEVELOPMENT.md) ·
[`docs/PRODUCT.md`](Balance/docs/PRODUCT.md)
- **Android (`BalanceAndroid/`):** [`AGENTS.md`](BalanceAndroid/AGENTS.md) ·
[`docs/ARCHITECTURE.md`](BalanceAndroid/docs/ARCHITECTURE.md) ·
[`docs/DATA_MODEL.md`](BalanceAndroid/docs/DATA_MODEL.md) ·
[`docs/SYNC.md`](BalanceAndroid/docs/SYNC.md) ·
[`docs/DEVELOPMENT.md`](BalanceAndroid/docs/DEVELOPMENT.md) ·
[`docs/PRODUCT.md`](BalanceAndroid/docs/PRODUCT.md)
- **Server (`BalanceServer/`):** [`AGENTS.md`](BalanceServer/AGENTS.md) ·
[`docs/ARCHITECTURE.md`](BalanceServer/docs/ARCHITECTURE.md) ·
[`docs/API.md`](BalanceServer/docs/API.md) ·
[`docs/DATA_MODEL.md`](BalanceServer/docs/DATA_MODEL.md) ·
[`docs/SYNC.md`](BalanceServer/docs/SYNC.md) ·
[`docs/SECURITY.md`](BalanceServer/docs/SECURITY.md) ·
[`docs/DEVELOPMENT.md`](BalanceServer/docs/DEVELOPMENT.md) ·
[`docs/DEPLOYMENT.md`](BalanceServer/docs/DEPLOYMENT.md) ·
[`docs/PRODUCT.md`](BalanceServer/docs/PRODUCT.md)
## Структура репозитория
```text
Balance/
├── AGENTS.md # общие правила для агентов по всему репо
├── README.md # этот файл
├── docs/ # сводная документация по всем платформам
├── Balance/ # Xcode-проект: iOS + macOS + watchOS
├── BalanceAndroid/ # Android (Gradle/Kotlin)
└── BalanceServer/ # Go-сервер + встроенный web-клиент
```
+85
View File
@@ -0,0 +1,85 @@
# Архитектура Balance (обзор всей системы)
Этот документ описывает, как связаны между собой три подпроекта.
Детальная архитектура каждой платформы — в `docs/ARCHITECTURE.md`
соответствующего подпроекта.
## Общая схема
```text
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ iOS клиент │ │ macOS клиент│ │watchOS клиент│ Apple: SwiftUI + SwiftData
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ (Balance/)
│ │ │
└────────┬────────┴────────┬────────┘
│ │
HTTPS (или HTTP только для localhost)
│ │
┌────────┴────────┐ │
│ Android клиент │ │ Kotlin + Jetpack Compose + Room
│ (BalanceAndroid)│ │ (BalanceAndroid/)
└────────┬────────┘ │
│ │
▼ ▼
┌───────────────────────────────┐
│ BalanceServer │ Go 1.25 + SQLite
│ ┌───────────┬───────────────┐ │ (BalanceServer/)
│ │ HTTP API │ Web-клиент │ │
│ │ /v1/* │ (embedded) │ │
│ └───────────┴───────────────┘ │
└───────────────────────────────┘
```
Каждый нативный клиент (iOS/macOS/watchOS/Android) хранит полную копию
данных локально (SwiftData или Room) и работает офлайн. Сервер —
единственная точка правды при конфликте между устройствами одного
аккаунта: он хранит append-only лог изменений с курсором и применяет
last-write-wins по времени изменения записи (`updatedAt`/`syncUpdatedAt`).
Web-клиент не является отдельным подпроектом — это статические файлы,
встроенные (embed) в Go-бинарник сервера, использующие тот же HTTP API,
что и нативные клиенты, но с cookie-based сессией вместо Bearer-токена.
## Компоненты сервера
- **HTTP API** (`internal/httpapi/`) — auth-эндпоинты и `/v1/sync`.
- **auth** (`internal/auth/`) — пароли (Argon2id), JWT access/refresh
токены.
- **database** (`internal/database/`) — SQLite-хранилище, схема,
индексы.
- **config** (`internal/config/`) — переменные окружения.
- **web** (`internal/httpapi/web.go`) — раздача встроенного
web-приложения.
Подробнее — [`BalanceServer/docs/ARCHITECTURE.md`](../BalanceServer/docs/ARCHITECTURE.md).
## Компоненты Apple-клиента
- **Models** — SwiftData `@Model` сущности.
- **Utilities/FinanceCalculations** — единый источник правил расчёта
баланса и аналитики.
- **Shared/ServerSync** — HTTP-клиент, авторизация, sync orchestration,
Keychain.
- **Views** — общий SwiftUI UI; `BalanceMac/` и `BalanceWatch/` —
платформенно-специфичные точки входа и экраны.
Подробнее — [`Balance/docs/ARCHITECTURE.md`](../Balance/docs/ARCHITECTURE.md).
## Компоненты Android-клиента
- **data/** — Room entities, DAO, `FinanceMath` (та же роль, что и
`FinanceCalculations` в Apple-клиенте).
- **sync/** — `ServerApi` (HTTP/auth), `ServerPreferences` (URL/cursor),
`SecureSessionStore` (Android Keystore), `SyncRepository`
(orchestration), `SyncWorker` (периодический фон).
- **ui/** — Jetpack Compose экраны, состояние из `MainViewModel`.
Подробнее — [`BalanceAndroid/docs/ARCHITECTURE.md`](../BalanceAndroid/docs/ARCHITECTURE.md).
## Принцип согласованности между платформами
Три клиента реализуют одну и ту же доменную модель и один и тот же
sync-протокол независимо друг от друга, на разных стеках. Это
осознанный компромисс (нативный UX на каждой платформе) с обратной
стороной: любое изменение общего контракта нужно вносить согласованно
в трёх местах. См. `docs/SYNC_PROTOCOL.md` и `docs/DATA_MODEL.md`.
+108
View File
@@ -0,0 +1,108 @@
# Сквозная модель данных
Три платформы реализуют одну и ту же доменную модель независимо, на
своём стеке хранения. Этот документ сопоставляет поля между ними.
Полные описания — [`Balance/docs/DATA_MODEL.md`](../Balance/docs/DATA_MODEL.md),
[`BalanceAndroid/docs/DATA_MODEL.md`](../BalanceAndroid/docs/DATA_MODEL.md),
[`BalanceServer/docs/DATA_MODEL.md`](../BalanceServer/docs/DATA_MODEL.md).
## Транзакция
| Логическое поле | Apple (`FinanceTransaction`) | Android (`TransactionEntity`) | Сервер |
| --- | --- | --- | --- |
| Идентификатор | `id: UUID` | `id: String` (UUID) | stable ID |
| Сумма | `amount: Double` | `amount: Double` | amount |
| Дата операции | `date: Date` | `date: Long` (epoch millis) | date |
| Заметка | `note: String` | `note: String` | note |
| Имя категории (snapshot) | `categoryName: String` | `categoryName: String` | category presentation data |
| Иконка категории | `categoryIcon: String` (SF Symbol) | `categoryIcon: String` | — |
| Emoji категории | `categoryEmoji: String` | `categoryEmoji: String` | — |
| Цвет категории | `categoryColorName: String` | `categoryColorName: String` | — |
| Корректировка баланса | `isBalanceAdjustment: Bool` | `isBalanceAdjustment: Boolean` | balance-adjustment flag |
| Тип операции | `kindRawValue: String` (`income`/`expense`) | `kindRawValue: String` | transaction kind |
| Версия/время изменения | `syncUpdatedAt: Date` | `updatedAt: Long` | `updatedAt` |
| Владелец | сессия аккаунта | сессия аккаунта | принадлежность пользователю |
**Категория хранится snapshot'ом**, а не ссылкой, на всех платформах:
это позволяет не терять отображение старой операции при удалении
категории и синхронизировать запись без relationship. Из-за этого
переименование пользовательской категории должно явно обновлять уже
существующие транзакции и бюджеты — и Apple-, и Android-клиент это
делают.
## Пользовательская категория
| Логическое поле | Apple (`CustomCategory`) | Android (`categories`) | Сервер |
| --- | --- | --- | --- |
| Идентификатор | `id: UUID` | `id` | stable ID |
| Имя | `name: String` | `name` | name |
| Иконка | `icon: String` | `icon` | icon/emoji |
| Emoji | `emoji: String` | `emoji` | — |
| Цвет | `colorName: String` | `colorName` | color |
| Тип | `kindRawValue: String` | `kindRawValue` | operation kind |
| Создание | `createdAt: Date` | `createdAt` | creation metadata |
| Версия | `syncUpdatedAt: Date` | `updatedAt` | update metadata |
Имя пользовательской категории уникально без учёта регистра/диакритики
относительно системных и других пользовательских категорий (проверяется
на клиентах). Встроенные системные категории (Продукты, Транспорт,
Зарплата и т.д.) не персистятся как отдельные записи ни на одной
платформе — они закодированы в клиентском коде.
## Месячный бюджет
| Логическое поле | Apple (`MonthlyBudget`) | Android (`budgets`) | Сервер |
| --- | --- | --- | --- |
| Идентификатор | `id: UUID` | `id` | stable ID |
| Имя категории | `categoryName: String` | `categoryName` | category information |
| Иконка/emoji категории | `categoryIcon`, `categoryEmoji` | `categoryIcon`, `categoryEmoji` | — |
| Лимит | `limit: Double` | `limitAmount: Double` | monthly limit |
| Начало месяца | `monthStart: Date` | `monthStart: Long` | month start |
| Версия | `syncUpdatedAt: Date` | `updatedAt: Long` | `updatedAt` |
Бюджет идентифицируется собственным UUID, но связывается с категорией
по сохранённому имени (`categoryName`), а не по ссылке.
## Tombstone / deletion journal
| Логическое поле | Apple (`SyncTombstone`) | Android (`deletion_journal`) | Сервер (change record) |
| --- | --- | --- | --- |
| Идентификатор записи | `id: UUID` | `id = entity:recordId` | record ID |
| Тип сущности | `entity: String` | `entity` | entity |
| ID удалённой записи | `recordID: String` | `recordId` | id |
| Время | `updatedAt: Date` | `updatedAt: Long` | `updatedAt` |
| Флаг удаления | подразумевается (`deleted: true` в payload) | подразумевается | `deleted: true` |
Поддерживаемые значения `entity` одинаковы на всех платформах:
`transaction`, `category`, `budget`.
## Финансовые инварианты (одинаковы на Apple и Android)
1. `amount` не может быть отрицательным через обычный UI.
2. Общий баланс = `sum(income.amount) − sum(expense.amount)`; записи с
`isBalanceAdjustment == true` **учитываются** в этой сумме.
3. Периодическая аналитика **исключает** balance adjustments и записи
вне выбранного периода.
4. Расходы группируются по `categoryName` (не по ID категории).
5. `syncUpdatedAt`/`updatedAt` обязано меняться при каждом изменении
синхронизируемой сущности.
6. Удаление синхронизируемой сущности обязано создать tombstone —
прямой `DELETE`/`delete()` без tombstone запрещён.
Источники правды по расчётам:
`Balance/Balance/Utilities/FinanceCalculations.swift` (Apple) и
`BalanceAndroid/.../data/FinanceMath.kt` (Android). При изменении
правил на одной платформе перенеси изменение на другую.
## Инварианты на стороне сервера
1. Record ID стабилен между синхронизациями.
2. Запись принадлежит ровно одному пользователю; клиент не может
читать/изменять чужие записи.
3. `sequence` (серверный порядок изменений) не должен уменьшаться.
4. Повторная доставка одного и того же изменения не должна повреждать
данные (идемпотентность).
5. Удаление обязано быть видимым клиенту через следующий pull.
6. Все timestamps хранятся в UTC.
7. Enum/строковые значения (`kindRawValue` и т.п.) совместимы между
iOS и Android — не переименовывать без периода совместимости.
+106
View File
@@ -0,0 +1,106 @@
# Разработка (все платформы)
Сводная шпаргалка по сборке, запуску и проверке всех трёх подпроектов.
Подробности — в `docs/DEVELOPMENT.md` каждого подпроекта.
## Требования по платформам
| Подпроект | Инструменты | Минимальные версии |
| --- | --- | --- |
| `Balance/` | Xcode, Swift | Xcode 16+, iOS 17+, macOS 14+, watchOS 10+ |
| `BalanceAndroid/` | Android Studio / Gradle | AGP 8.13, Kotlin 2.3.21, compileSdk 36, minSdk 26, Java 17 |
| `BalanceServer/` | Go | Go 1.25.0+ |
## Локальный запуск всей системы
Порядок, удобный для end-to-end проверки sync между платформами:
```bash
# 1. Поднять сервер
cd BalanceServer
cp .env.example .env
# задать BALANCE_JWT_SECRET (см. README.md)
go mod tidy
go run ./cmd/balance-server
# сервер слушает http://localhost:8080
# 2. Открыть Apple-проект и указать http://localhost:8080 в Settings → Server account
cd ../Balance
open Balance.xcodeproj
# 3. Собрать и запустить Android-приложение
cd ../BalanceAndroid
./gradlew assembleDebug
# на эмуляторе сервер локального хоста доступен как http://10.0.2.2:8080
```
Для физического iPhone/Android-устройства `localhost` указывает на само
устройство, а не на машину разработчика — используйте сетевой адрес
или HTTPS reverse proxy (см. `docs/SECURITY.md`).
## Сборка и тесты по отдельности
### Apple (`Balance/`)
```bash
xcodebuild -project Balance.xcodeproj -list
xcodebuild -project Balance.xcodeproj -scheme Balance \
-destination 'generic/platform=iOS' build
xcodebuild -project Balance.xcodeproj -scheme Balance \
-destination 'platform=iOS Simulator,name=<available iPhone>' test
```
Схемы: `Balance` (iOS), `BalanceMac`, `BalanceWatch`. Без
`BalanceCloudKitEnabled=YES` приложение работает в local-mode
(SwiftData без CloudKit) — рекомендуемый режим для разработки.
### Android (`BalanceAndroid/`)
```bash
./gradlew clean test assembleDebug
```
Тесты — `app/src/test/FinanceMathTest`. `local.properties` с `sdk.dir`
не должен попадать в Git.
### Server (`BalanceServer/`)
```bash
go mod download
go test ./...
go test -race ./...
go vet ./...
go build ./...
```
Docker-путь — `docker compose up -d --build` (см.
[`BalanceServer/README.md`](../BalanceServer/README.md)).
## Что проверять при изменении общего контракта
Если PR меняет sync API, модель данных или финансовые расчёты — прогони
проверки во всех трёх подпроектах, а не только в изменённом:
```text
old iOS client + new server
new iOS client + server
Android client + server
concurrent sync с двух устройств одного аккаунта
login/refresh после истечения access token
delete → pull на другом устройстве
```
## Типовые проблемы синхронизации (одинаковый чек-лист для обеих платформ)
1. Проверь server URL в настройках клиента.
2. Проверь правило HTTPS/localhost (см. `docs/SECURITY.md`).
3. Проверь авторизацию/сессию (login, не истёк ли refresh).
4. Проверь дату последней синхронизации и сохранённый cursor.
5. Проверь совместимость версии клиента и `BalanceServer`.
6. Если данные не удаляются на другом устройстве — убедись, что
удаление прошло через tombstone-путь
(`modelContext.deleteForSync` на Apple,
`FinanceDao.deleteTransaction/deleteCategory/deleteBudget` на
Android), а не через прямой `delete()`.
+95
View File
@@ -0,0 +1,95 @@
# Безопасность (сквозные требования)
Этот документ собирает требования безопасности, которые применяются
ко **всем** подпроектам одновременно. Платформенно-специфичные детали —
в [`BalanceServer/docs/SECURITY.md`](../BalanceServer/docs/SECURITY.md),
`Balance/AGENTS.md` (раздел «Безопасность») и
`BalanceAndroid/AGENTS.md` (раздел «Security»).
## Хранение секретов на клиентах
| | Apple | Android |
| --- | --- | --- |
| Access/refresh токены | Keychain (service `BalanceServer`) | зашифрованное хранилище через Android Keystore (AES/GCM), `SecureSessionStore` |
| Несекретные настройки (URL сервера, cursor, sync metadata) | `UserDefaults` | `ServerPreferences` |
| Запрещено | пароль в `UserDefaults`, SwiftData или файлах | токены в обычных `SharedPreferences` |
Ни один клиент не должен хранить пароль пользователя после
аутентификации — только access/refresh токены.
## Хранение секретов на сервере
- Пароли — только Argon2id-хеши, plaintext никогда не сохраняется.
- Refresh-токены хранятся как SHA-256 хеши, не в открытом виде.
- `BALANCE_JWT_SECRET` должен быть случайной строкой не короче 32
символов; его смена инвалидирует access-токены (refresh-сессии
остаются валидны и получают новый access-токен при следующем
refresh).
## Транспорт
- Для удалённого (не-localhost) сервера все клиенты обязаны
использовать HTTPS.
- HTTP разрешён клиентам **только** для `localhost`, `127.0.0.1` и
`::1` — это относится и к Apple-, и к Android-клиенту.
- Production-деплой сервера должен работать через HTTPS/TLS
(например, через reverse proxy — Caddy/nginx), см.
[`BalanceServer/README.md#https`](../BalanceServer/README.md#https).
## Логирование
Запрещено логировать на любой из трёх платформ:
- пароли;
- access/refresh токены;
- заголовок `Authorization` целиком;
- полные request bodies с финансовыми данными без явной необходимости.
Безопасно логировать (на сервере): request ID, endpoint, status code,
latency, размер batch, неконфиденциальные идентификаторы записей.
## Авторизация на сервере
Каждая sync-мутация обязана проверять ownership по authenticated user
из токена/сессии. Никогда не доверяй `userId`, пришедшему в теле
запроса от клиента, если identity уже известна из
Authorization-заголовка или cookie-сессии:
```text
Authorization header / session cookie
↓
authenticated user
↓
server-side ownership check
↓
requested entity
```
## Валидация входных данных (сервер)
Проверяются: UUID/ID, enum-значения, `amount`, timestamps, длины строк,
размер batch, размер JSON, границы cursor/sequence.
## Защита от злоупотреблений (production-деплой сервера)
- rate limiting для auth-эндпоинтов (встроенный лимитер — 30
auth-запросов на IP в минуту; для более сильного публичного
rate limiting используйте reverse proxy);
- разумные ограничения на размер тела запроса;
- таймауты и graceful shutdown;
- ограничение подключений к БД;
- мониторинг.
## Секреты в репозитории
Ни в одном из трёх подпроектов не должны появляться:
- production URL серверов;
- реальные `BALANCE_JWT_SECRET` или иные секреты;
- закоммиченные `.env`, keystore-файлы, сертификаты;
- захардкоженные тестовые credentials, которые могут быть спутаны с
боевыми.
`.env.example` в `BalanceServer/` и `local.properties.example` в
`BalanceAndroid/` — единственные допустимые «примерные» файлы с
конфигурацией; реальные значения в них не хранятся.
+88
View File
@@ -0,0 +1,88 @@
# Единый протокол синхронизации
Все клиенты (iOS, macOS, watchOS, Android, web) синхронизируются с
`BalanceServer` через один и тот же HTTP(S) API и один и тот же набор
инвариантов. Это сводка контракта; детальные описания на стороне
сервера — [`BalanceServer/docs/API.md`](../BalanceServer/docs/API.md) и
[`BalanceServer/docs/SYNC.md`](../BalanceServer/docs/SYNC.md).
Клиентские реализации — [`Balance/docs/SYNC.md`](../Balance/docs/SYNC.md)
и [`BalanceAndroid/docs/SYNC.md`](../BalanceAndroid/docs/SYNC.md).
## Эндпоинты
| Метод | Путь | Назначение |
| --- | --- | --- |
| `GET` | `/health` | Health check |
| `POST` | `/v1/auth/register` | Создание аккаунта |
| `POST` | `/v1/auth/login` | Вход, выдача access/refresh токенов |
| `POST` | `/v1/auth/refresh` | Ротация токенов |
| `POST` | `/v1/auth/logout` | Отзыв refresh-токена |
| `GET` | `/v1/me` | Текущий аккаунт |
| `POST` | `/v1/sync` | Push и pull изменений |
Нативные клиенты используют `Authorization: Bearer <access-token>`.
Web-клиент использует `HttpOnly`/`SameSite=Strict` cookie-сессию —
это единственное отличие транспорта, сама sync-семантика идентична.
## Основные инварианты
1. **Cursor** — каждый клиент хранит курсор последнего успешно
применённого изменения. Курсор сохраняется только после успешного
применения соответствующего batch, не раньше.
2. **Sequence** — сервер поддерживает глобальный монотонный порядок
изменений в change stream; порядок не должен уменьшаться и должен
быть однозначным при конкурентных запросах.
3. **Push** — клиент отправляет свои изменения (`POST /v1/sync`);
сервер валидирует, проверяет владение записью (ownership) и
добавляет изменение в change stream. Повторный push (retry) не
должен создавать вторую логическую запись из одного и того же
client-change (идемпотентность).
4. **Pull** — клиент запрашивает изменения после своего курсора;
сервер никогда не возвращает чужие данные (изоляция по аккаунту) и
поддерживает постраничный pull с продолжением от нового курсора.
5. **Conflict resolution** — last-write-wins по `updatedAt`
(`syncUpdatedAt` на Apple, `updatedAt` на Android/сервере). Сервер —
единственный источник правды в этом сравнении, поэтому не должен
произвольно перезаписывать `updatedAt` при каждом чтении.
6. **Удаления (tombstones)** — удаление сущности не является простым
`DELETE`; оно создаёт запись изменения вида
`{ "entity": ..., "id": ..., "deleted": true, "updatedAt": ... }`,
которая проходит через тот же change stream, что и обычные
изменения. На Apple это `SyncTombstone`/`modelContext.deleteForSync`,
на Android — `deletion_journal` через DAO-методы `deleteTransaction` /
`deleteCategory` / `deleteBudget`.
7. **Batch** — Apple-клиент отправляет батчи до 100 изменений и может
делить крупные payload'ы; сервер не должен предполагать, что все
изменения аккаунта приходят одним запросом. Android должен
оставаться совместимым с этим же батчингом.
8. **Reset/full sync** — если клиент теряет курсор, сервер должен
поддерживать предусмотренный API механизм полного pull или новый
baseline-курсор. Неизвестный/просроченный курсор нельзя молча
трактовать как актуальный.
## Синхронизируемые сущности
- **Транзакция** (доход/расход, сумма, дата, заметка, snapshot
категории);
- **Пользовательская категория** (имя, иконка/emoji, цвет);
- **Месячный бюджет** (категория, месяц, лимит);
- **Tombstone / deletion journal** запись для удалений.
Подробное пополевое сопоставление — [`docs/DATA_MODEL.md`](DATA_MODEL.md).
## Что нельзя менять без координации
Любое из следующих изменений — breaking change для всех клиентов
одновременно, даже если каждый подпроект по отдельности компилируется
и проходит собственные тесты:
- имена/типы JSON-полей в payload'ах `/v1/sync` и auth-эндпоинтов;
- HTTP-методы или пути эндпоинтов;
- семантика курсора/sequence;
- правило разрешения конфликтов (last-write-wins по `updatedAt`);
- формат tombstone-записи.
При таком изменении обнови одновременно: `BalanceServer` (API +
хендлеры + `docs/API.md`/`docs/SYNC.md`), `Balance/Shared/ServerSync.swift`
(+ `Balance/docs/SYNC.md`), `BalanceAndroid/.../sync/*`
(+ `BalanceAndroid/docs/SYNC.md`) и этот файл.