Files
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

15 KiB
Raw Permalink Blame History

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) нет — не добавляй их без явной необходимости.

Структура

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 не переименовывай: это сломает настройки у существующих пользователей.

Сборка и проверка

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.