diff --git a/BalanceAndroid/AGENTS.md b/BalanceAndroid/AGENTS.md new file mode 100644 index 0000000..ae3b7e7 --- /dev/null +++ b/BalanceAndroid/AGENTS.md @@ -0,0 +1,136 @@ +# AGENTS.md + +## Проект + +**Balance Android** — нативный Android-клиент приложения Balance для учёта личных финансов. Он повторяет доменную модель iOS/macOS/watchOS-клиентов и синхронизируется с тем же self-hosted BalanceServer. + +Стек: +- Kotlin 2.3.21 +- Jetpack Compose +- Room +- Coroutines / Flow +- WorkManager +- Android Keystore +- minSdk 26 / compileSdk 36 +- Java 17 bytecode + +## Структура + +```text +BalanceAndroid/ +├── app/src/main/java/com/example/balanceandroid/ +│ ├── data/ # Room entities, DAO, финансовая математика +│ ├── sync/ # HTTP API, session, preferences, sync repository/worker +│ ├── ui/ # Compose UI и theme +│ ├── MainActivity.kt +│ ├── MainViewModel.kt +│ └── BalanceApplication.kt +├── app/src/test/ # unit tests +├── app/src/main/res/ +└── docs/ +``` + +## Правила + +### Data layer + +- Все persisted финансовые сущности находятся в `data/`. +- Доступ к Room выполняется через `FinanceDao`. +- Не помещай HTTP или Compose-логику в DAO. +- Изменение Room entities требует проверки schema/migration. +- `updatedAt` — timestamp версии записи и обязателен для syncable entities. + +### Финансовая логика + +`FinanceMath` является единым источником правил расчёта. + +- balance = income − expense; +- balance adjustments влияют на общий баланс; +- adjustments исключаются из периодической аналитики; +- spending группируется по `categoryName`. + +При изменении правил обновляй `FinanceMathTest`. + +### Удаления + +Для синхронизируемых объектов нельзя использовать только прямой `DELETE`. + +Используй DAO-методы: +- `deleteTransaction` +- `deleteCategory` +- `deleteBudget` + +Они сначала создают запись в `deletion_journal`, затем удаляют объект. + +### Категории + +Категория хранится snapshot-полями в transactions/budgets. Поэтому при переименовании пользовательской категории текущая реализация обновляет связанные операции и бюджеты. Сохраняй это поведение. + +### Sync + +`SyncRepository` — единственная точка orchestration синхронизации. + +- `ServerApi` отвечает только за HTTP/auth. +- `ServerPreferences` отвечает за URL, cursor и sync metadata. +- `SecureSessionStore` отвечает за зашифрованную сессию. +- `SyncWorker` запускает периодическую sync-задачу. + +Не делай network calls непосредственно из Compose UI. + +### Security + +- Access/refresh tokens не должны попадать в обычные SharedPreferences. +- Не логируй tokens/passwords/Authorization. +- Session хранится через AES/GCM с ключом Android Keystore. +- Для удалённого сервера нужен HTTPS. +- HTTP разрешён только локальным адресам, предусмотренным `ServerPreferences`. +- Не добавляй credentials или production secrets в репозиторий. + +### Compose + +- UI state идёт из `MainViewModel`. +- Данные Room наблюдаются через Flow. +- Общие компоненты — `ui/Components.kt`; feature screens — `ui/BalanceRoot.kt` и `ui/Editors.kt`. +- Не переносить бизнес-логику в composables. + +### Sync invariants + +При изменении syncable entity: +1. сохранить новое значение; +2. обновить `updatedAt`; +3. дождаться следующего sync. + +При удалении: +1. создать deletion journal; +2. удалить локальную запись; +3. отправить tombstone на сервер. + +Remote change применяется только если его `updatedAt` новее локального. + +## Тестирование + +Минимум: + +```bash +./gradlew test +``` + +Перед PR желательно: + +```bash +./gradlew clean test assembleDebug +``` + +Если менялся sync/data layer, добавь unit tests для новых инвариантов. + +## PR checklist + +- [ ] Изменён правильный слой. +- [ ] Финансовая логика остаётся в `FinanceMath`. +- [ ] Изменения persisted schema учтены. +- [ ] `updatedAt` обновляется. +- [ ] Deletes создают tombstones. +- [ ] Sync не вызывается напрямую из UI. +- [ ] Нет секретов в репозитории/logcat. +- [ ] Unit tests проходят. +- [ ] README/docs обновлены при изменении setup/API. diff --git a/BalanceAndroid/README.md b/BalanceAndroid/README.md index 564bbdb..8d50c3a 100644 --- a/BalanceAndroid/README.md +++ b/BalanceAndroid/README.md @@ -94,3 +94,13 @@ export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" Проект задаёт для Gradle 2 ГБ heap, 1 ГБ metaspace и не более двух параллельных workers. Эти параметры находятся в `gradle.properties` и применяются также при сборке из Android Studio. Debug APK появится в `app/build/outputs/apk/debug/`. + +## Документация проекта + +- [`AGENTS.md`](AGENTS.md) — правила для AI-агентов и разработчиков. +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — архитектура Android-клиента. +- [`docs/DATA_MODEL.md`](docs/DATA_MODEL.md) — Room-модели и финансовые правила. +- [`docs/SYNC.md`](docs/SYNC.md) — протокол синхронизации и безопасность сессии. +- [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) — Android Studio, Gradle, тесты и release. +- [`docs/PRODUCT.md`](docs/PRODUCT.md) — функциональная документация. + diff --git a/BalanceAndroid/docs/ARCHITECTURE.md b/BalanceAndroid/docs/ARCHITECTURE.md new file mode 100644 index 0000000..eb97e91 --- /dev/null +++ b/BalanceAndroid/docs/ARCHITECTURE.md @@ -0,0 +1,101 @@ +# Архитектура Balance Android + +## Слои + +```text +Jetpack Compose + │ + ▼ + MainViewModel + │ + ├──────────────► FinanceMath + │ + ▼ + FinanceDao ────────► Room / SQLite + │ + └──────────────► SyncRepository + │ + ┌─────────┴─────────┐ + ▼ ▼ + ServerApi SecureSessionStore + │ │ + ▼ ▼ + BalanceServer Android Keystore +``` + +## Application + +`BalanceApplication` создаёт singleton-зависимости: + +- `AppDatabase` +- `ServerPreferences` +- `SecureSessionStore` +- `SyncRepository` + +Также регистрирует уникальный WorkManager job `balance-periodic-sync`. + +## UI + +`MainActivity` запускает Compose root. + +`BalanceRoot` содержит навигацию между: +- Обзор; +- Аналитика; +- Операции; +- Бюджеты; +- Настройки; +- Категории; +- Сервер и синхронизация. + +`MainViewModel` объединяет Room flows, sync status/session и настройки в `FinanceUiState`. + +## Data + +Room database называется `balance-android.db`. + +Entities: +- `TransactionEntity` +- `CategoryEntity` +- `BudgetEntity` +- `DeletionEntity` + +`FinanceDao` предоставляет Flow для UI и suspend-операции для записи/sync. + +## Sync + +Sync двухфазный: + +```text +local changes → push +server changes → pull +``` + +Push идёт пакетами до 100 изменений. Большой JSON (> 1.5 MB) рекурсивно разбивается. + +Pull использует server cursor и `hasMore`. + +Remote changes применяются атомарно через Room transaction. + +## Conflict resolution + +Стратегия — timestamp-based last-write-wins: + +```text +remote.updatedAt > local.updatedAt +``` + +Если локальная запись новее или равна, remote payload не заменяет её. + +## Background sync + +WorkManager запускается с: +- периодом 15 минут; +- `NetworkType.CONNECTED`. + +Worker ничего не делает без server session/server URL. + +Ограничения Android могут отложить фактический запуск; 15 минут — минимальный период WorkManager, а не гарантия точного времени запуска. + +## Платформа + +Android-клиент не использует CloudKit. Локальное хранение всегда Room; синхронизация с другими клиентами выполняется через BalanceServer. diff --git a/BalanceAndroid/docs/DATA_MODEL.md b/BalanceAndroid/docs/DATA_MODEL.md new file mode 100644 index 0000000..b294839 --- /dev/null +++ b/BalanceAndroid/docs/DATA_MODEL.md @@ -0,0 +1,111 @@ +# Модель данных + +## Room schema + +### transactions + +`TransactionEntity`: + +| Поле | Тип | Назначение | +|---|---|---| +| `id` | String | UUID | +| `amount` | Double | сумма | +| `date` | Long | epoch millis | +| `note` | String | заметка | +| `categoryName` | String | snapshot категории | +| `categoryIcon` | String | иконка | +| `categoryEmoji` | String | emoji | +| `categoryColorName` | String | цвет | +| `isBalanceAdjustment` | Boolean | корректировка | +| `kindRawValue` | String | `income`/`expense` | +| `updatedAt` | Long | версия для sync | + +Индексы: `updatedAt`, `date`. + +### categories + +Пользовательские категории: + +- `id` +- `name` +- `icon` +- `emoji` +- `colorName` +- `kindRawValue` +- `createdAt` +- `updatedAt` + +Индекс `(name, kindRawValue)` помогает находить категории по имени и типу. + +### budgets + +- `id` +- `categoryName` +- `categoryIcon` +- `categoryEmoji` +- `limitAmount` +- `monthStart` +- `updatedAt` + +### deletion_journal + +Локальный журнал tombstones: + +- `id = entity:recordId` +- `entity` +- `recordId` +- `updatedAt` + +## Built-in categories + +Expense: +- Продукты +- Транспорт +- Дом +- Развлечения +- Здоровье +- Покупки +- Подписки +- Образование +- Другое + +Income: +- Зарплата +- Подработка +- Инвестиции +- Подарок +- Другое + +Они не являются Room entities. + +## Финансовые правила + +### Общий баланс + +```text +sum(income.amount) - sum(expense.amount) +``` + +`isBalanceAdjustment` здесь не исключается. + +### Периодическая аналитика + +`FinanceMath.summary` исключает: +- все balance adjustments; +- записи вне `[start, endExclusive)`. + +### Spending + +`FinanceMath.spending`: +- берёт только expenses; +- исключает adjustments; +- группирует по category name; +- сортирует по убыванию суммы. + +## Category snapshot + +Transaction и budget не имеют Room relationship на CategoryEntity. Они хранят presentation data непосредственно. + +Это позволяет удалять пользовательскую категорию, не разрушая исторические операции. + +Поэтому rename категории распространяется вручную на связанные transactions/budgets. diff --git a/BalanceAndroid/docs/DEVELOPMENT.md b/BalanceAndroid/docs/DEVELOPMENT.md new file mode 100644 index 0000000..02cabb8 --- /dev/null +++ b/BalanceAndroid/docs/DEVELOPMENT.md @@ -0,0 +1,154 @@ +# Разработка + +## Toolchain + +Текущая версия проекта: + +- Android Studio Quail 1 (2026.1.1) или новее +- Gradle 8.13 +- Android Gradle Plugin 8.13.0 +- Kotlin 2.3.21 +- Compose BOM 2026.06.00 +- compileSdk 36 +- minSdk 26 +- Java 17 target +- Room 2.8.4 +- Lifecycle 2.10.0 + +Проект не использует сторонние runtime-библиотеки вне стандартного Android/Jetpack stack. + +## Запуск + +Откройте `BalanceAndroid/` в Android Studio. + +Затем: + +1. Gradle Sync. +2. Установить Android SDK Platform 36. +3. Создать/подключить Android 8+ устройство или emulator. +4. Запустить configuration `app`. + +## CLI + +```bash +./gradlew test assembleDebug +``` + +Полная чистая проверка: + +```bash +./gradlew clean test assembleDebug +``` + +Проверить JVM: + +```bash +./gradlew --version +``` + +Используется Java 17–23; bytecode target — Java 17. + +## SDK + +Можно использовать: + +```bash +export ANDROID_HOME="/path/to/Android/Sdk" +export ANDROID_SDK_ROOT="$ANDROID_HOME" +``` + +или `local.properties`: + +```properties +sdk.dir=/home/USER/Android/Sdk +``` + +`local.properties` не должен попадать в Git. + +## Unit tests + +Тесты находятся в: + +```text +app/src/test/ +``` + +Основной текущий тестовый класс: + +```text +FinanceMathTest +``` + +Тестируй отдельно: +- balance; +- period summary; +- adjustment exclusion; +- spending grouping; +- edge cases around period boundaries. + +## Local development + +Приложение работает без сервера. Локальные данные хранятся в Room. + +Для Android Emulator host server: + +```text +http://10.0.2.2:8080 +``` + +Для физического Android device `localhost` указывает на сам телефон, поэтому нужен доступный по сети HTTPS endpoint. + +## Application ID + +Текущий пример: + +```text +com.example.balanceandroid +``` + +Перед Google Play release замени его на уникальный production application ID. + +## Release checklist + +- [ ] Production applicationId. +- [ ] Release signing key. +- [ ] HTTPS server. +- [ ] Проверен backup/data extraction policy. +- [ ] Проверена миграция Room. +- [ ] Проверена background sync. +- [ ] Проверена авторизация и token refresh. +- [ ] Нет секретов в APK source/config. +- [ ] `./gradlew clean test assembleDebug` проходит. + +## Troubleshooting + +### Gradle не находит SDK + +Проверь `ANDROID_HOME`, `ANDROID_SDK_ROOT` или `local.properties`. + +### Неверная Java + +Android Studio → Gradle JDK → Embedded JDK. + +Нужна Java 17–23. + +### Старый Gradle daemon + +```bash +./gradlew --stop +./gradlew clean test assembleDebug +``` + +### Sync не работает + +Проверь: +1. server URL; +2. HTTPS/local HTTP rule; +3. login/session; +4. network connection; +5. cursor/lastPush; +6. совместимость BalanceServer protocol. + +### Данные не удаляются на другом устройстве + +Убедись, что вызывается `FinanceDao.deleteTransaction/deleteCategory/deleteBudget`, а не `delete*Direct`. diff --git a/BalanceAndroid/docs/PRODUCT.md b/BalanceAndroid/docs/PRODUCT.md new file mode 100644 index 0000000..b717e19 --- /dev/null +++ b/BalanceAndroid/docs/PRODUCT.md @@ -0,0 +1,107 @@ +# Функциональная документация + +## Обзор + +Balance Android — локально-first приложение личных финансов. + +Основные функции: +- доходы; +- расходы; +- категории; +- бюджеты; +- аналитика; +- корректировка баланса; +- server sync; +- тёмная/светлая тема; +- несколько валют. + +## Обзор + +Показывает: +- общий баланс; +- доходы текущего месяца; +- расходы текущего месяца; +- расходы по категориям; +- последние операции. + +## Операции + +Поддерживаются: +- добавление; +- редактирование; +- удаление; +- поиск; +- фильтрация по income/expense; +- дата; +- сумма; +- категория; +- заметка. + +## Корректировка + +Пользователь задаёт целевой фактический баланс. + +Приложение вычисляет: + +```text +difference = target - current +``` + +и создаёт специальную transaction с `isBalanceAdjustment=true`. + +Корректировка меняет общий баланс, но не включается в периодическую аналитику. + +## Аналитика + +Периоды: +- месяц; +- 3 месяца; +- 12 месяцев. + +Показываются: +- доходы; +- расходы; +- результат; +- savings rate; +- структура расходов по категориям. + +## Бюджеты + +Можно задать месячный лимит для категории. + +Бюджет хранится отдельно и синхронизируется с сервером. + +## Категории + +Можно создавать и редактировать пользовательские категории. + +Поддерживаются: +- имя; +- тип; +- иконка; +- emoji; +- цвет. + +Есть 50 вариантов иконок, 30 готовых emoji и палитра из 24 цветов согласно текущему UI. + +## Настройки + +- RUB; +- EUR; +- USD; +- SEK; +- system/light/dark; +- server URL; +- регистрация/login/logout; +- ручная sync; +- reset sync. + +## Offline-first + +Все основные операции выполняются локально через Room. Сервер нужен только для авторизации и межустройственной синхронизации. + +## Background sync + +При наличии аккаунта и server URL WorkManager пытается синхронизировать данные примерно каждые 15 минут при наличии сети. + +Точное время зависит от Android WorkManager и системных ограничений батареи/фоновой работы. diff --git a/BalanceAndroid/docs/SYNC.md b/BalanceAndroid/docs/SYNC.md new file mode 100644 index 0000000..e14cf4a --- /dev/null +++ b/BalanceAndroid/docs/SYNC.md @@ -0,0 +1,212 @@ +# Серверная синхронизация + +Android-клиент использует тот же self-hosted BalanceServer, что и остальные клиенты Balance. + +## API + +POST endpoints: + +```text +/v1/auth/register +/v1/auth/login +/v1/auth/logout +/v1/auth/refresh +/v1/sync +``` + +Авторизованный запрос: + +```http +Authorization: Bearer +Content-Type: application/json; charset=utf-8 +``` + +## Session + +`ServerSession` содержит: + +- access token; +- refresh token; +- expiresAt; +- userId; +- email. + +`SecureSessionStore` сериализует session JSON и шифрует его AES/GCM. + +AES key создаётся и хранится в Android Keystore. + +При ошибке расшифровки store очищается. + +## Server URL + +`ServerPreferences.normalizeServer` принимает полный URL без: +- user info; +- query; +- fragment. + +Разрешены HTTPS и локальный HTTP. + +HTTP допускается для: +- `localhost` +- `127.0.0.1` +- `10.0.2.2` + +`10.0.2.2` — специальный адрес Android Emulator для host machine. + +## Authentication + +Перед авторизованным запросом клиент проверяет expiry. + +Если осталось менее 60 секунд, выполняется refresh. + +При HTTP 401 клиент один раз refresh-ит session и повторяет исходный запрос. + +Если refresh не удался, session очищается. + +## Sync request + +```json +{ + "cursor": 0, + "deviceId": "UUID", + "changes": [], + "pull": true +} +``` + +Push использует тот же endpoint с `pull=false`. + +## Change envelope + +```json +{ + "entity": "transaction", + "id": "UUID", + "deleted": false, + "updatedAt": "2026-09-08T10:00:00Z", + "payload": {} +} +``` + +Entities: + +```text +transaction +category +budget +``` + +## Transaction payload + +```json +{ + "amount": 1000, + "date": "2026-09-08T10:00:00Z", + "note": "Кофе", + "categoryName": "Кофе", + "categoryIcon": "cup.and.saucer.fill", + "categoryEmoji": "☕️", + "categoryColorName": "brown", + "isBalanceAdjustment": false, + "kindRawValue": "expense" +} +``` + +## Category payload + +```json +{ + "name": "Кофе", + "icon": "cup.and.saucer.fill", + "emoji": "☕️", + "colorName": "brown", + "kindRawValue": "expense", + "createdAt": "2026-09-01T10:00:00Z" +} +``` + +## Budget payload + +```json +{ + "categoryName": "Кофе", + "categoryIcon": "cup.and.saucer.fill", + "categoryEmoji": "☕️", + "limit": 5000, + "monthStart": "2026-09-01T00:00:00Z" +} +``` + +## Push + +Изменения выбираются по `updatedAt` между `lastPush` и cutoff. + +Batch size: 100. + +Если JSON batch больше 1.5 MB, он рекурсивно делится. + +## Pull + +Клиент отправляет текущий cursor. + +Сервер возвращает: + +```json +{ + "cursor": 42, + "changes": [], + "hasMore": false +} +``` + +Если `hasMore=true`, клиент повторяет pull с новым cursor. + +Есть защитные проверки: +- cursor не может уменьшиться; +- `hasMore=true` должен сопровождаться продвижением cursor. + +## Applying remote changes + +Изменения сортируются по `sequence`. + +Для обычного изменения: + +```text +apply remote iff remote.updatedAt > local.updatedAt +``` + +Для удаления: + +```text +delete iff local.updatedAt <= remote.updatedAt +``` + +После применения соответствующий deletion journal очищается. + +## Scope + +Cursor и lastPush привязаны к: + +```text +serverUrl + userId +``` + +Это позволяет иметь независимое состояние sync для разных серверов/аккаунтов. + +## Reset + +`resetAndSync()` очищает cursor и lastPush текущего scope, затем запускает обычную sync. + +Локальные финансовые данные при этом не удаляются. + +## Background + +`SyncWorker`: +- запускается WorkManager; +- требует network connection; +- пропускает работу без session/server URL; +- возвращает `retry`, если sync завершилась ошибкой. + +## Совместимость + +README проекта указывает совместимость Android-клиента с BalanceServer protocol 3.1. При изменении контракта сервера необходимо синхронно обновлять `ServerApi`, `SyncRepository` и документацию. diff --git a/BalanceAndroid/gradlew b/BalanceAndroid/gradlew old mode 100755 new mode 100644