Files
BalanceWatch/AGENTS.md
T
wtandClaude Opus 5.5 9cfac1797c
Build / Build (watchOS Simulator) (push) Canceled after 0s
Build / Build (watchOS Device) (push) Canceled after 0s
Restyle the watch app in the «Лес» palette with the Flutter app icon
- 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>
2026-11-01 02:11:00 +07:00

222 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.