- BalancePalette (Shared/AppTheme.swift): the palette of BalanceFlutter's default preset. Balance card in a green gradient with mint text, income teal (↙) and expense deep orange (↗) as in the Flutter app, mint accent and a forest «Добавить операцию» button instead of indigo/purple. - Money shows no kopecks when whole, otherwise exactly two decimals («8 480,50 ₽»), like the Flutter client. - App icon: same artwork as BalanceFlutter (branding/icon.svg), every size of AppIcon.appiconset rendered by the new branding/render.sh (resvg, bundled Roboto fonts). - Repository avatar and README banner redrawn in the same style as the BalanceFlutter banner, with real simulator screenshots (docs/). Checked on the watchOS 27.2 simulator (icon in the app grid, home and accounts screens). Builds for simulator and device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
15 KiB
Markdown
222 lines
15 KiB
Markdown
# AGENTS.md — BalanceWatch
|
||
|
||
Этот файл — точка входа для AI-агентов и разработчиков. Прочитай его
|
||
целиком перед любым изменением в репозитории.
|
||
|
||
## Что это за проект
|
||
|
||
**BalanceWatch** — самостоятельное (watch-only) приложение для учёта
|
||
личных финансов под **watchOS 10+**. Это клиент экосистемы Balance:
|
||
те же сущности и тот же протокол синхронизации используют iOS/macOS-,
|
||
Android- и web-клиенты, а синхронизируются все они через self-hosted
|
||
сервер BalanceServer.
|
||
|
||
Проект выделен из монорепозитория Balance: туда входили таргет
|
||
`BalanceWatch` и общий код, который этот таргет компилировал. Здесь
|
||
этот код живёт как **копия** (см. раздел «Общий контракт»).
|
||
|
||
- UI: SwiftUI, только системные компоненты.
|
||
- Хранилище: SwiftData, только локально (`cloudKitDatabase: .none`).
|
||
- Синхронизация: HTTP(S) к BalanceServer, сессия хранится в Keychain.
|
||
- Внешних зависимостей (SPM/CocoaPods) нет — не добавляй их без явной
|
||
необходимости.
|
||
|
||
## Структура
|
||
|
||
```text
|
||
BalanceWatch/
|
||
├── AGENTS.md
|
||
├── README.md
|
||
├── .github/workflows/build.yml # CI: сборка для симулятора и устройства
|
||
├── docs/CONTRACT_V2.md # общий контракт экосистемы v2 (копия)
|
||
├── branding/ # icon.svg, avatar, banner, render.sh; не входит в бандл
|
||
├── docs/ # CONTRACT_V2.md, скриншоты для баннера
|
||
├── BalanceWatch.xcodeproj/ # один таргет и одна схема — BalanceWatch
|
||
└── BalanceWatch/
|
||
├── App/
|
||
│ ├── BalanceWatchApp.swift # @main, ModelContainer, тема
|
||
│ ├── WatchRootView.swift # главный экран: баланс, месяц, навигация
|
||
│ ├── WatchAddTransactionView.swift # добавление операции (+ выбор счёта)
|
||
│ ├── WatchAccountsView.swift # CurrencyContext в окружении, счета,
|
||
│ │ # выбор пространства (ledger)
|
||
│ └── WatchFeatures.swift # аналитика, операции, бюджеты,
|
||
│ # категории, корректировка баланса, настройки
|
||
├── Models/ # SwiftData-модели и системные категории;
|
||
│ # V2Records.swift — счета, переводы, курсы, SyncRecord
|
||
├── Shared/ # BalanceModelContainer, AppTheme,
|
||
│ # CategoryDesignOptions, ServerSync (ServerAccountStore)
|
||
├── Utilities/ # FinanceCalculations, Money (сотые, курсы, конвертация),
|
||
│ # форматирование валют и дат
|
||
├── Views/Components/CategoryIconView.swift
|
||
└── Assets.xcassets
|
||
```
|
||
|
||
### Xcode-проект
|
||
|
||
`project.pbxproj` использует **папки, синхронизируемые с файловой
|
||
системой** (`PBXFileSystemSynchronizedRootGroup`, objectVersion 77,
|
||
нужен Xcode 16+). Любой файл, положенный в `BalanceWatch/`, сам
|
||
попадает в таргет:
|
||
|
||
- чтобы добавить файл, просто создай его в нужной папке, `project.pbxproj`
|
||
трогать не нужно;
|
||
- `project.pbxproj` редактируй только ради build settings;
|
||
- не клади в `BalanceWatch/` файлы, которые не должны попасть в
|
||
бандл (скрипты, документацию, тестовые данные).
|
||
|
||
Ключевые build settings: `SDKROOT = watchos`, `WATCHOS_DEPLOYMENT_TARGET = 10.0`,
|
||
`INFOPLIST_KEY_WKWatchOnly = YES`, `GENERATE_INFOPLIST_FILE = YES` (своего
|
||
Info.plist нет — ключи задаются через `INFOPLIST_KEY_*`), bundle id
|
||
`com.tolamironcenko.Balance.watchkitapp`, `SWIFT_VERSION = 5.0`.
|
||
|
||
## Правила разработки
|
||
|
||
### 1. Где что менять
|
||
|
||
| Что | Где |
|
||
| --- | --- |
|
||
| Экраны часов | `App/` |
|
||
| Модель данных | `Models/` + `Shared/BalanceModelContainer.swift` |
|
||
| Финансовые правила | `Utilities/FinanceCalculations.swift` |
|
||
| Авторизация и синхронизация | `Shared/ServerSync.swift` |
|
||
| Переиспользуемые view | `Views/Components/` |
|
||
|
||
Не дублируй доменную логику во view: расчёты — в `FinanceCalculations`,
|
||
сеть — в `ServerAccountStore`.
|
||
|
||
### 2. SwiftData
|
||
|
||
Схема зарегистрирована в `BalanceModelContainer.schema`:
|
||
`FinanceTransaction`, `MonthlyBudget`, `CustomCategory`, `SyncTombstone`,
|
||
`FinanceAccount`, `FinanceTransfer`, `ExchangeRate`, `SyncRecord`.
|
||
|
||
- Каждое пространство данных (§7 контракта) — отдельное хранилище:
|
||
личное — прежнее хранилище по умолчанию, общее —
|
||
`BalanceLedger-<scope>`, где scope — хеш сервер + пользователь + ledger
|
||
(`LedgerContainers`, `LedgerSelection`). Корневой view пересоздаётся
|
||
через `.id(scope)`.
|
||
- `syncID` — id записи на проводе. У записей, созданных до v2, он `nil`, и
|
||
отправляется прежний `id.uuidString` (иначе на сервере появятся дубли);
|
||
новые записи получают UUID в нижнем регистре, полученные — id с сервера.
|
||
- `extraPayload` хранит незнакомые поля payload и отправляет их обратно
|
||
(§0.2). `SyncRecord` хранит `recurring`, `goal`, `debt`, `attachment`,
|
||
`setting` целиком.
|
||
|
||
- У пользователей уже есть локальные базы. Не удаляй и не переименовывай
|
||
persisted-поля без стратегии миграции; новые поля делай со значением
|
||
по умолчанию или опциональными.
|
||
- Если основное хранилище не открылось, `make()` откатывается на
|
||
in-memory контейнер `BalanceRecovery`. Не убирай этот fallback.
|
||
- CloudKit не используется и не должен включаться: единственный
|
||
источник синхронизации — BalanceServer.
|
||
|
||
### 3. Изменение и удаление записей
|
||
|
||
- Любое изменение синхронизируемой сущности обязано обновлять
|
||
`syncUpdatedAt`, иначе изменение не уйдёт на сервер.
|
||
- Удаляй синхронизируемые сущности через
|
||
`modelContext.deleteForSync(...)`, а не через `modelContext.delete(...)`:
|
||
первый вариант пишет tombstone в `SyncDeletionStore`, и удаление
|
||
доходит до других устройств.
|
||
- Операции и бюджеты хранят snapshot категории (имя, иконка, emoji,
|
||
цвет). При переименовании пользовательской категории связанные
|
||
операции и бюджеты нужно обновить — сохраняй это поведение.
|
||
|
||
### 4. Финансовые правила
|
||
|
||
Центральные правила — в `FinanceCalculations`:
|
||
|
||
- суммы считаются в целых сотых (`Money`), курсы — десятичные строки,
|
||
округление половиной от нуля; интервалы полуоткрытые `[начало, конец)`;
|
||
- общий баланс = доходы − расходы + переводы, всё в базовой валюте
|
||
(`CurrencyContext`: `setting baseCurrency`, иначе `@AppStorage("currencyCode")`);
|
||
нет курса — сумма не входит, показывается «нет курса C→B»;
|
||
- баланс счёта — в его валюте без конвертации;
|
||
- операции с `isBalanceAdjustment == true` входят в общий баланс, но
|
||
**не** входят в периодическую аналитику (месяц/3 мес./12 мес.);
|
||
- расходы группируются по `categoryName`.
|
||
|
||
Эти правила **одинаковы во всех клиентах Balance** (Apple, Android, web)
|
||
и описаны в `docs/CONTRACT_V2.md` §2–3. Не меняй их только здесь.
|
||
|
||
### 5. Синхронизация (общий контракт)
|
||
|
||
`ServerAccountStore.shared` — единственная точка входа для авторизации и
|
||
синхронизации (`register`, `login`, `logout`, `sync`). Эндпоинты — `v1/...`
|
||
на BalanceServer; sync устроен как push изменений после последнего push
|
||
(транзакции → категории → бюджеты → tombstones), затем pull по `cursor`
|
||
пачками (`syncBatchSize`, `hasMore`).
|
||
|
||
- Формат JSON (`SyncChange`, `SyncRequest`, `SyncResponse`, payload'ы
|
||
сущностей), семантика cursor/sequence и auth-заголовки — **общий
|
||
контракт со всеми клиентами и сервером**. Изменение здесь без
|
||
изменения сервера и остальных клиентов сломает синхронизацию.
|
||
- При смене URL сервера сессия очищается, cursor/push-state
|
||
сбрасываются, пользователь логинится заново (`saveServer`). Сохраняй
|
||
это поведение. Смена сервера и выход возвращают личное пространство.
|
||
- v2: перед синхронизацией запрашивается `GET /v1/capabilities`; 404 —
|
||
сервер v1, тогда `supportsV2 == false`: счета и общие бюджеты скрыты,
|
||
`setting` не отправляется. Незнакомые сущности при pull пропускаются.
|
||
- Общее пространство передаётся заголовком `X-Balance-Ledger` на `v1/sync`;
|
||
cursor, push-state и журнал удалений имеют суффикс `LedgerSelection.keySuffix`.
|
||
- Из сущностей v2 часы отправляют только `setting` (базовая валюта);
|
||
счета, переводы, курсы, регулярные операции и прочее только читают.
|
||
Регулярные операции часы не создают.
|
||
|
||
### 6. Безопасность
|
||
|
||
- Пароли не сохраняются нигде. Access/refresh токены — только в Keychain
|
||
(`KeychainStore`), никогда не в `UserDefaults`, SwiftData или файлах.
|
||
- Для удалённого сервера — только HTTPS. HTTP разрешён только для
|
||
`localhost`, `127.0.0.1`, `::1`.
|
||
- Не логируй пароли, токены и заголовок `Authorization`.
|
||
- Не коммить секреты, production-URL и credentials.
|
||
|
||
### 7. UI на часах
|
||
|
||
- Предпочитай `NavigationStack`, `List`, `Form`, короткие списки и
|
||
компактные действия; крупный текст с `minimumScaleFactor`.
|
||
- Строки интерфейса — на русском (`developmentRegion = ru`).
|
||
- Акцентный цвет — `.indigo`; тема берётся из `@AppStorage("appTheme")`
|
||
через `AppTheme`. Валюта — `@AppStorage("currencyCode")`.
|
||
- Ключи `@AppStorage`/`UserDefaults` не переименовывай: это сломает
|
||
настройки у существующих пользователей.
|
||
|
||
## Сборка и проверка
|
||
|
||
```bash
|
||
xcodebuild -project BalanceWatch.xcodeproj -scheme BalanceWatch \
|
||
-destination 'generic/platform=watchOS Simulator' \
|
||
CODE_SIGNING_ALLOWED=NO build
|
||
```
|
||
|
||
CI (`.github/workflows/build.yml`) собирает проект на `macos-15` для
|
||
симулятора и для устройства без подписи. Перед PR сборка должна быть
|
||
зелёной в обоих вариантах.
|
||
|
||
Тестового таргета пока нет. `Utilities/Money.swift` не зависит от
|
||
SwiftData/SwiftUI, а `FinanceCalculations` работает через протоколы
|
||
`LedgerEntry`/`LedgerTransfer`, поэтому денежную математику можно проверить
|
||
отдельным `swiftc`-прогоном на macOS (с заглушкой `FinanceTransaction`). Если добавляешь тесты, не завязывай их на
|
||
текущую дату и локаль — явно фиксируй `Calendar`, `Date` и formatter'ы.
|
||
Добавив тестовый таргет, добавь и шаг `test` в CI.
|
||
|
||
## Чек-лист перед PR
|
||
|
||
- [ ] Изменение лежит в правильном слое (`App/`, `Models/`, `Shared/`, `Utilities/`).
|
||
- [ ] Persisted-поля SwiftData не сломаны, миграция продумана.
|
||
- [ ] Изменённые синхронизируемые сущности обновляют `syncUpdatedAt`.
|
||
- [ ] Удаления идут через `deleteForSync`.
|
||
- [ ] Финансовые правила и sync-контракт не разошлись с остальными клиентами Balance.
|
||
- [ ] Секреты и токены не попали в репозиторий и логи.
|
||
- [ ] CI собирает проект для симулятора и для устройства.
|
||
- [ ] README/AGENTS.md обновлены, если поменялась структура или сборка.
|
||
|
||
## Оформление
|
||
|
||
- Цвета интерфейса — только из `BalancePalette` (`Shared/AppTheme.swift`),
|
||
палитра «Лес», общая с BalanceFlutter. Не возвращай indigo/purple.
|
||
- Иконка, аватар и баннер рендерятся из SVG: `bash branding/render.sh`
|
||
(нужен `resvg`). PNG в `AppIcon.appiconset` руками не правь.
|
||
- Аватар репозитория в Gitea обновляется из `branding/avatar.png`.
|