added docs and AGENTS.md to Balance for ios,macos,watchos
This commit is contained in:
1 parent
579a572097
commit
896cc48128
7 files changed
+907
No files matched your search
@@ -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=<available iPhone>' 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.
|
||||||
@@ -94,3 +94,15 @@ CloudKit является опциональным режимом. Для нег
|
|||||||
## Устранение проблем сборки
|
## Устранение проблем сборки
|
||||||
|
|
||||||
После перехода с версии 2.0–2.0.2 выполните **Product → Clean Build Folder**. Если Xcode продолжает использовать старую конфигурацию watchOS target, закройте Xcode и удалите DerivedData для проекта `Balance`, затем снова откройте проект.
|
После перехода с версии 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`.
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
Из-за этого переименование пользовательской категории должно явно обновлять связанные записи — именно так работает текущая реализация.
|
||||||
@@ -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=<available iPhone>' \
|
||||||
|
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(...)`.
|
||||||
@@ -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.
|
||||||
@@ -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 <access-token>
|
||||||
|
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 нужно обновить до версии, совместимой с текущим архивом.
|
||||||
|
|
||||||
|
Клиент и сервер желательно обновлять согласованно.
|
||||||
Reference in new issue
Block a user