diff --git a/Balance/AGENTS.md b/Balance/AGENTS.md new file mode 100644 index 0000000..854de96 --- /dev/null +++ b/Balance/AGENTS.md @@ -0,0 +1,146 @@ +# AGENTS.md + +## Назначение проекта + +**Balance** — нативное мультиплатформенное приложение для учёта личных финансов: +- iOS 17+ +- macOS 14+ +- watchOS 10+ + +Основной UI написан на SwiftUI, локальное хранилище — SwiftData. Проект поддерживает два взаимоисключающих способа синхронизации данных: CloudKit или собственный HTTP(S)-сервер. + +## Структура репозитория + +```text +Balance/ +├── Balance/ # Общий код iOS/macOS/watchOS +│ ├── App/ # Точка входа iOS и корневой ContentView +│ ├── Models/ # SwiftData-модели и доменные типы +│ ├── Shared/ # Общее состояние, контейнер SwiftData, server sync +│ ├── Utilities/ # Чистые функции расчётов и форматирование +│ └── Views/ # Общие SwiftUI-экраны +├── BalanceMac/ # macOS entry point и macOS-specific UI +├── BalanceWatch/ # watchOS entry point и компактный UI +├── BalanceTests/ # XCTest +├── Config/ # Entitlements +├── Balance.xcodeproj/ +└── docs/ # Архитектура, модель данных, sync и dev guide +``` + +## Правила разработки + +### 1. Сначала определяй слой изменения + +- **Модель/данные:** `Balance/Models/` +- **Расчёты:** `Balance/Utilities/FinanceCalculations.swift` +- **Общий сервис:** `Balance/Shared/` +- **Общий UI:** `Balance/Views/` +- **Только Mac:** `BalanceMac/` +- **Только Watch:** `BalanceWatch/` +- **Тесты:** `BalanceTests/` + +Не дублируй доменную логику в iOS/macOS/watchOS-экранах, если её можно выразить общей функцией или сервисом. + +### 2. SwiftData + +Текущая схема зарегистрирована в `BalanceModelContainer.schema`: + +- `FinanceTransaction` +- `MonthlyBudget` +- `CustomCategory` +- `SyncTombstone` + +При изменении `@Model` учитывай миграции существующих локальных баз. Не удаляй и не переименовывай persisted-поля без явной стратегии миграции. + +### 3. Изменения финансовых записей + +Любая редактируемая сервером сущность должна обновлять `syncUpdatedAt`. + +Для удаления используй: + +```swift +modelContext.deleteForSync(...) +``` + +а не прямой `modelContext.delete(...)`, если сущность должна удалиться и на других устройствах. Это создаёт tombstone в `SyncDeletionStore`. + +### 4. Баланс и аналитика + +`FinanceCalculations` содержит центральные правила: + +- общий баланс = доходы − расходы; +- `isBalanceAdjustment == true` учитывается в общем балансе; +- корректировки не учитываются в периодической аналитике; +- расходы группируются по `categoryName`. + +Не меняй эти правила только в одном UI. При изменении поведения обновляй `BalanceTests/FinanceCalculationsTests.swift`. + +### 5. Категории + +Системные категории находятся в `FinanceCategory`. + +Пользовательские категории — `CustomCategory`. Их имя является важной частью текущей модели: операции и бюджеты хранят snapshot имени/иконки/emoji/цвета. При переименовании пользовательской категории текущий код обновляет связанные операции и бюджеты — это поведение нужно сохранять. + +### 6. Синхронизация + +`ServerAccountStore` — единая точка серверной авторизации и синхронизации. + +Не хранить пароль в `UserDefaults`, SwiftData или файлах. Сессия хранится в Keychain. + +При изменении URL сервера: +1. текущая сессия очищается; +2. cursor/push state удаляется; +3. пользователь должен авторизоваться заново. + +Не включай одновременно CloudKit и собственный сервер как два источника синхронизации одной и той же базы. + +### 7. Безопасность + +- Для удалённого сервера использовать HTTPS. +- HTTP разрешён клиентом только для `localhost`, `127.0.0.1` и `::1`. +- Access/refresh tokens — только Keychain. +- Не логируй токены, пароли или полные Authorization headers. +- Не добавляй секреты, production URLs или credentials в репозиторий. + +### 8. UI + +Используй SwiftUI и системные компоненты Apple. + +- Общие компоненты размещай в `Balance/Views/Components/`. +- Не тащи macOS-only API в общий код без `#if os(macOS)`. +- Не тащи watchOS-only API в общий код без условной компиляции. +- На Watch предпочитай короткие списки, `Form`, `List`, `NavigationStack` и компактные действия. + +### 9. Тестирование + +Минимум перед PR: + +```bash +xcodebuild -project Balance.xcodeproj -scheme Balance -destination 'platform=iOS Simulator,name=' test +``` + +Также полезно проверить Mac и Watch schemes: + +```bash +xcodebuild -project Balance.xcodeproj -scheme BalanceMac -showBuildSettings +xcodebuild -project Balance.xcodeproj -scheme BalanceWatch -showBuildSettings +``` + +Если имя симулятора отличается, сначала получи список: + +```bash +xcrun simctl list devices available +``` + +Не добавляй тесты, которые зависят от текущей даты или локали без явной фиксации `Calendar`, `Date` и formatter. + +## Commit/PR checklist + +- [ ] Изменение находится в правильном target/layer. +- [ ] Нет дублирования финансовой логики. +- [ ] Все серверные изменения сущностей обновляют `syncUpdatedAt`. +- [ ] Удаления синхронизируемых сущностей используют tombstone. +- [ ] Изменения расчётов покрыты XCTest. +- [ ] Проверены iOS/macOS/watchOS-условия компиляции. +- [ ] Не добавлены секреты и токены. +- [ ] Документация обновлена, если изменились данные, sync API или setup. diff --git a/Balance/README.md b/Balance/README.md index a8f7595..3ef01d0 100644 --- a/Balance/README.md +++ b/Balance/README.md @@ -94,3 +94,15 @@ CloudKit является опциональным режимом. Для нег ## Устранение проблем сборки После перехода с версии 2.0–2.0.2 выполните **Product → Clean Build Folder**. Если Xcode продолжает использовать старую конфигурацию watchOS target, закройте Xcode и удалите DerivedData для проекта `Balance`, затем снова откройте проект. + +## Документация проекта + +- [`AGENTS.md`](AGENTS.md) — правила для AI-агентов и разработчиков, структура проекта, invariants и checklist. +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — архитектура приложения и ответственность слоёв. +- [`docs/DATA_MODEL.md`](docs/DATA_MODEL.md) — SwiftData-модели и финансовые инварианты. +- [`docs/SYNC.md`](docs/SYNC.md) — протокол авторизации и синхронизации с собственным сервером. +- [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) — локальная разработка, сборка, тесты и signing. +- [`docs/PRODUCT.md`](docs/PRODUCT.md) — функциональная документация. + +> Примечание: текущий архив содержит Apple-клиент Balance. Внешний Go-сервер, упомянутый в старой версии README, в этом архиве отсутствует; `docs/SYNC.md` документирует клиентский контракт по коду `ServerSync.swift`. + diff --git a/Balance/docs/ARCHITECTURE.md b/Balance/docs/ARCHITECTURE.md new file mode 100644 index 0000000..16e8e18 --- /dev/null +++ b/Balance/docs/ARCHITECTURE.md @@ -0,0 +1,139 @@ +# Архитектура Balance + +## Обзор + +Balance использует простую feature-oriented структуру поверх SwiftUI + SwiftData: + +```text +SwiftUI Views + │ + ├── FinanceCalculations + │ + ├── SwiftData ModelContext / @Query + │ + └── ServerAccountStore + │ + ├── Keychain (session) + ├── UserDefaults (server URL, cursor, sync metadata) + └── HTTP(S) API /v1/* +``` + +Общий код находится в `Balance/Balance/`. Платформенные оболочки находятся в `BalanceMac/` и `BalanceWatch/`. + +## Точки входа + +| Platform | Entry point | Основной root | +|---|---|---| +| iOS | `Balance/App/BalanceApp.swift` | `ContentView` | +| macOS | `BalanceMac/BalanceMacApp.swift` | `MacContentView` | +| watchOS | `BalanceWatch/BalanceWatchApp.swift` | `WatchRootView` | + +Каждое приложение создаёт `BalanceModelContainer.make()` и передаёт его через `.modelContainer(...)`. + +## Хранение данных + +`BalanceModelContainer` выбирает конфигурацию: + +1. если `BalanceCloudKitEnabled == true` и CloudKit container создаётся — SwiftData + CloudKit; +2. иначе — локальная SwiftData база; +3. если локальная база не создаётся — используется in-memory recovery container. + +Recovery container предназначен для предотвращения падения приложения при невозможности открыть persistent store; он не является способом восстановления данных. + +## Модели + +### FinanceTransaction + +Основная финансовая операция: + +- UUID; +- сумма `Double`; +- дата; +- заметка; +- snapshot категории; +- тип `income` / `expense`; +- флаг корректировки баланса; +- `syncUpdatedAt`. + +`TransactionKind` сериализуется через `kindRawValue`, а наружу предоставляется computed property `kind`. + +### CustomCategory + +Пользовательская категория: + +- имя; +- SF Symbol или emoji; +- цвет; +- тип операции; +- `createdAt`; +- `syncUpdatedAt`. + +### MonthlyBudget + +Месячный лимит для категории: + +- snapshot категории; +- `limit`; +- `monthStart`; +- `syncUpdatedAt`. + +### SyncTombstone + +SwiftData-модель для совместимости схемы, но клиентский журнал удалений фактически ведётся через `SyncDeletionStore` в `UserDefaults`. Это сделано специально, чтобы удаление не зависело от миграции основной SwiftData-схемы. + +## Расчёты + +`FinanceCalculations` — чистый доменный слой: + +- `totalBalance` считает весь баланс; +- `monthInterval` возвращает календарный месяц; +- `summary` считает доходы/расходы периода; +- `spendingByCategory` агрегирует расходы. + +Корректировки (`isBalanceAdjustment`) влияют на `totalBalance`, но исключаются из `summary` и `spendingByCategory`. + +## UI/features + +Общие features: + +- Dashboard +- Transactions +- Budgets +- Analytics +- Categories +- Settings +- Balance Adjustment +- CSV export +- Server account/sync + +macOS добавляет полноценную desktop-навигацию и расширенный аналитический экран. + +watchOS использует отдельные экраны с теми же моделями и расчётами, но адаптированным UI. + +## Состояние приложения + +`@Query` используется как основной способ наблюдения за SwiftData. Feature views не должны создавать собственные параллельные кеши финансовых сущностей без необходимости. + +Глобальное состояние серверного аккаунта сосредоточено в `ServerAccountStore.shared`. + +Настройки пользователя хранятся через `@AppStorage`, в частности: + +- `currencyCode`; +- `appTheme`; +- server URL; +- sync cursor; +- last push/sync timestamps. + +## Принцип изменения + +Предпочтительный поток: + +```text +UI action + → update/insert/delete SwiftData + → update syncUpdatedAt / tombstone + → UI обновляется через @Query + → ServerAccountStore синхронизирует изменения +``` + +Не следует делать HTTP-запросы непосредственно из feature view. diff --git a/Balance/docs/DATA_MODEL.md b/Balance/docs/DATA_MODEL.md new file mode 100644 index 0000000..be2d59b --- /dev/null +++ b/Balance/docs/DATA_MODEL.md @@ -0,0 +1,97 @@ +# Модель данных + +## Persisted schema + +```text +FinanceTransaction +CustomCategory +MonthlyBudget +SyncTombstone +``` + +## FinanceTransaction + +| Поле | Тип | Назначение | +|---|---|---| +| `id` | UUID | стабильный идентификатор | +| `amount` | Double | положительная сумма | +| `date` | Date | дата операции | +| `note` | String | заметка | +| `categoryName` | String | snapshot имени категории | +| `categoryIcon` | String | SF Symbol | +| `categoryEmoji` | String | emoji, если используется | +| `categoryColorName` | String | имя цвета | +| `isBalanceAdjustment` | Bool | специальная корректировка | +| `kindRawValue` | String | `income` / `expense` | +| `syncUpdatedAt` | Date | версия изменения | + +## CustomCategory + +| Поле | Тип | +|---|---| +| `id` | UUID | +| `name` | String | +| `icon` | String | +| `emoji` | String | +| `colorName` | String | +| `kindRawValue` | String | +| `createdAt` | Date | +| `syncUpdatedAt` | Date | + +Имя пользовательской категории уникально без учёта регистра/диакритики относительно системных и других пользовательских категорий. + +При редактировании категории текущая реализация распространяет новое имя и presentation metadata на связанные транзакции и бюджеты. + +## MonthlyBudget + +| Поле | Тип | +|---|---| +| `id` | UUID | +| `categoryName` | String | +| `categoryIcon` | String | +| `categoryEmoji` | String | +| `limit` | Double | +| `monthStart` | Date | +| `syncUpdatedAt` | Date | + +Бюджет идентифицируется UUID, но связывается с категорией по сохранённому `categoryName`. + +## SyncTombstone + +| Поле | Тип | +|---|---| +| `id` | UUID | +| `entity` | String | +| `recordID` | String | +| `updatedAt` | Date | + +В текущем клиентском sync-flow tombstone удалений хранится в `SyncDeletionStore` (`UserDefaults`) и отправляется как `deleted: true`. + +Поддерживаемые entity: + +```text +transaction +category +budget +``` + +## Финансовые инварианты + +1. `amount` не должен быть отрицательным через пользовательский UI. +2. Тип операции хранится в `kindRawValue`. +3. Баланс: + `sum(income.amount) - sum(expense.amount)`. +4. Корректировка баланса является обычной транзакцией для общего баланса, но не участвует в периодической аналитике. +5. `syncUpdatedAt` должен меняться при каждом изменении синхронизируемой сущности. +6. Удаляемая синхронизируемая сущность должна создавать tombstone. + +## Почему категория хранится snapshot'ом + +Операция хранит `categoryName`, `categoryIcon`, `categoryEmoji` и `categoryColorName`, а не relationship на `CustomCategory`. + +Это позволяет: +- не терять отображение старой операции при удалении категории; +- синхронизировать запись без SwiftData relationship; +- поддерживать системные категории без persisted object. + +Из-за этого переименование пользовательской категории должно явно обновлять связанные записи — именно так работает текущая реализация. diff --git a/Balance/docs/DEVELOPMENT.md b/Balance/docs/DEVELOPMENT.md new file mode 100644 index 0000000..e2ba56a --- /dev/null +++ b/Balance/docs/DEVELOPMENT.md @@ -0,0 +1,180 @@ +# Разработка + +## Требования + +- Xcode 16+ +- Swift 5 +- iOS 17+ +- macOS 14+ +- watchOS 10+ + +В проекте нет Swift Package Manager-зависимостей. + +## Открытие + +```bash +open Balance.xcodeproj +``` + +Доступные shared schemes: + +```text +Balance +BalanceMac +BalanceWatch +``` + +## Сборка + +Сначала можно посмотреть схемы: + +```bash +xcodebuild -project Balance.xcodeproj -list +``` + +Для CI используйте явный `-scheme` и подходящий `-destination`. + +Пример проверки компиляции: + +```bash +xcodebuild \ + -project Balance.xcodeproj \ + -scheme Balance \ + -destination 'generic/platform=iOS' \ + build +``` + +Для Mac: + +```bash +xcodebuild \ + -project Balance.xcodeproj \ + -scheme BalanceMac \ + -destination 'generic/platform=macOS' \ + build +``` + +Для Watch: + +```bash +xcodebuild \ + -project Balance.xcodeproj \ + -scheme BalanceWatch \ + -destination 'generic/platform=watchOS' \ + build +``` + +## Тесты + +Основной тестовый target: + +```text +BalanceTests +``` + +Запуск: + +```bash +xcodebuild \ + -project Balance.xcodeproj \ + -scheme Balance \ + -destination 'platform=iOS Simulator,name=' \ + test +``` + +Тесты сосредоточены на финансовых расчётах и sync deletion journal. + +## Signing + +Проект рассчитан на запуск с собственной Team. + +Для обычной локальной разработки: +- выберите свою Team в Signing & Capabilities; +- используйте локальное SwiftData; +- CloudKit можно не включать. + +Bundle identifiers текущего проекта: + +```text +com.tolamironcenko.Balance +com.tolamironcenko.Balance.mac +com.tolamironcenko.Balance.watchkitapp +``` + +При публикации их следует заменить на собственные identifiers. + +## Local mode + +Без `BalanceCloudKitEnabled=YES` приложение использует локальное SwiftData-хранилище. + +Это рекомендуемый режим для: +- UI-разработки; +- unit tests; +- Personal Team; +- отладки без Apple Developer Program. + +## CloudKit mode + +CloudKit является опциональным. + +Нужно: +1. иметь платную Apple Developer Team; +2. создать собственный CloudKit container; +3. заменить placeholder container ID в entitlements; +4. включить iCloud/CloudKit capabilities; +5. выставить `BalanceCloudKitEnabled=YES`. + +Не смешивайте CloudKit и собственный сервер как два активных sync backends для одной пользовательской базы. + +## Server mode + +В Settings укажите URL сервера, затем зарегистрируйтесь или войдите. + +Для физического устройства удалённый адрес должен быть HTTPS. + +Для локального сервера допустим: + +```text +http://localhost:... +http://127.0.0.1:... +``` + +На физическом iPhone `localhost` означает сам iPhone, а не Mac-разработчика. + +## Экспорт CSV + +CSV export реализован в: + +```text +Balance/Views/Settings/CSVDocument.swift +``` + +Экспорт доступен на iOS и macOS. + +## Troubleshooting + +### Xcode использует старые build artifacts + +```text +Product → Clean Build Folder +``` + +Если проблема сохраняется, удалите DerivedData проекта и заново откройте Xcode. + +### SwiftData schema error + +Проверьте изменения `@Model` и не меняйте persisted fields без migration strategy. + +### Sync не работает + +Проверьте по порядку: +1. server URL; +2. HTTPS/localhost rule; +3. авторизацию; +4. дату последней синхронизации; +5. cursor; +6. совместимость версии Balance Server. + +### Данные не удаляются на другом устройстве + +Проверьте, что удаление выполнено через `deleteForSync(...)`, а не прямой `delete(...)`. diff --git a/Balance/docs/PRODUCT.md b/Balance/docs/PRODUCT.md new file mode 100644 index 0000000..d0c903d --- /dev/null +++ b/Balance/docs/PRODUCT.md @@ -0,0 +1,101 @@ +# Функциональная документация + +## Dashboard + +Показывает: +- общий баланс; +- доходы текущего месяца; +- расходы текущего месяца; +- последние операции; +- быстрый переход к аналитике, операциям, бюджетам, категориям, корректировке и настройкам. + +## Операции + +Поддерживаются: +- доходы; +- расходы; +- категория; +- сумма; +- дата; +- заметка; +- редактирование; +- удаление; +- поиск; +- фильтрация по типу. + +## Корректировка баланса + +Пользователь задаёт фактический баланс. + +Разница между фактическим и текущим балансом сохраняется как специальная операция с `isBalanceAdjustment=true`. + +Такая запись: +- влияет на общий баланс; +- не влияет на месячные income/expenses; +- не попадает в spending-by-category. + +Это позволяет исправить расхождение между приложением и реальным счётом, не искажая историю расходов. + +## Аналитика + +Доступны периоды: +- месяц; +- 3 месяца; +- 12 месяцев. + +Используются: +- доходы; +- расходы; +- результат; +- savings rate; +- структура расходов по категориям; +- динамика доходов и расходов. + +## Бюджеты + +Для категории можно задать месячный лимит. + +Budget хранится отдельно от транзакций и синхронизируется через собственную entity `budget`. + +## Категории + +Системные категории разделены на income/expense. + +Пользователь может создать свою категорию: +- название; +- тип; +- SF Symbol или emoji; +- цвет. + +Имя категории не должно дублировать существующее имя. + +## Настройки + +- валюта: RUB, EUR, USD, SEK; +- тема: system/light/dark; +- сервер и синхронизация; +- CSV export на iOS/macOS. + +## watchOS + +Watch-приложение предоставляет компактные версии: +- Dashboard; +- Add/Edit transaction; +- All transactions; +- Analytics; +- Budgets; +- Categories; +- Balance adjustment; +- Settings. + +## macOS + +Mac-приложение предоставляет: +- dashboard; +- полную аналитику; +- операции; +- бюджеты; +- категории; +- корректировку; +- настройки; +- CSV export. diff --git a/Balance/docs/SYNC.md b/Balance/docs/SYNC.md new file mode 100644 index 0000000..e9d7847 --- /dev/null +++ b/Balance/docs/SYNC.md @@ -0,0 +1,232 @@ +# Серверная синхронизация + +## Назначение + +`Balance/Shared/ServerSync.swift` реализует авторизацию и двустороннюю синхронизацию с собственным сервером. + +Сервер не входит в этот Xcode-проект; клиент ожидает API под базовым URL сервера. + +## Хранилище сессии + +Сессия содержит: + +- access token; +- refresh token; +- expiry; +- user id/email. + +Сессия хранится в Keychain с service `BalanceServer`. + +В `UserDefaults` хранятся только несекретные sync-настройки: + +- server URL; +- device ID; +- cursor; +- last push; +- last sync. + +Пароль клиента не сохраняется. + +## Endpoints + +Клиент использует POST JSON: + +```text +POST /v1/auth/register +POST /v1/auth/login +POST /v1/auth/logout +POST /v1/auth/refresh +POST /v1/sync +``` + +Для авторизованных запросов используется: + +```http +Authorization: Bearer +Content-Type: application/json +``` + +При HTTP 401 клиент один раз обновляет access token через refresh endpoint и повторяет запрос. + +## Валидация server URL + +Разрешены: + +- `https://...` +- `http://localhost...` +- `http://127.0.0.1...` +- `http://[::1]...` + +Удалённый HTTP запрещён. + +При смене адреса сервера клиент: +- удаляет сессию; +- удаляет sync cursor/push state; +- требует новую авторизацию. + +## Sync request + +Логическая форма: + +```json +{ + "cursor": 123, + "deviceId": "UUID", + "changes": [], + "pull": true +} +``` + +Для push `changes` заполнен, `pull=false`. + +## Sync change + +```json +{ + "entity": "transaction", + "id": "UUID", + "deleted": false, + "updatedAt": "ISO-8601", + "payload": {} +} +``` + +Также сервер может возвращать `version` и `sequence`. + +Поддерживаемые entity: + +```text +transaction +category +budget +``` + +## Payloads + +### transaction + +```json +{ + "amount": 1000, + "date": "2026-09-08T10:00:00.000Z", + "note": "Кофе", + "categoryName": "Кофе", + "categoryIcon": "cup.and.saucer.fill", + "categoryEmoji": "☕️", + "categoryColorName": "brown", + "isBalanceAdjustment": false, + "kindRawValue": "expense" +} +``` + +### category + +```json +{ + "name": "Кофе", + "icon": "cup.and.saucer.fill", + "emoji": "☕️", + "colorName": "brown", + "kindRawValue": "expense", + "createdAt": "2026-09-01T10:00:00.000Z" +} +``` + +### budget + +```json +{ + "categoryName": "Кофе", + "categoryIcon": "cup.and.saucer.fill", + "categoryEmoji": "☕️", + "limit": 5000, + "monthStart": "2026-09-01T00:00:00.000Z" +} +``` + +Даты кодируются как ISO-8601; клиент принимает варианты с fractional seconds и без них. + +## Push + +Клиент отправляет: +1. изменённые transactions; +2. изменённые categories; +3. изменённые budgets; +4. tombstones. + +Размер batch — до 100 записей. + +Если тело запроса превышает 1.5 MB, batch делится рекурсивно. Один payload больше 256 KB считается слишком большим. + +## Pull + +Клиент отправляет `pull=true` с cursor и применяет изменения по `sequence`. + +После успешного применения сохраняется новый cursor. + +Если сервер сообщает `hasMore=true`, клиент продолжает pagination до завершения. + +Клиент защищается от: +- уменьшения cursor; +- `hasMore=true` без продвижения cursor. + +## Conflict resolution + +При применении входящего изменения запись обновляется только если: + +```text +local.syncUpdatedAt < remote.updatedAt +``` + +Для удаления используется аналогичная проверка: + +```text +local.syncUpdatedAt <= remote.updatedAt +``` + +Это простая last-write-wins стратегия на уровне timestamp. + +## Deletions + +Удаление локально: + +```swift +modelContext.deleteForSync(transaction) +``` + +создаёт запись в `SyncDeletionStore`. + +При sync она отправляется как: + +```json +{ + "entity": "transaction", + "id": "...", + "deleted": true, + "updatedAt": "..." +} +``` + +После получения соответствующего server change tombstone удаляется из локального журнала. + +## Полная пересинхронизация + +`resetSynchronization()` очищает cursor и last-push timestamp текущего server/user context, после чего выполняет обычный sync. + +Это не удаляет локальные финансовые записи автоматически. + +## Автосинхронизация + +Apple clients запускают sync: +- после авторизации; +- при запуске/появлении root view; +- затем примерно каждые 120 секунд, пока цикл жив; +- вручную из Settings. + +Фактическая фоновая работа всё равно зависит от lifecycle ОС. + +## Совместимость + +Клиент специально распознаёт `invalid_json` на `/v1/sync` и сообщает, что Balance Server нужно обновить до версии, совместимой с текущим архивом. + +Клиент и сервер желательно обновлять согласованно.