added AGENTS.ms and docs for Android app

This commit is contained in:
wt
2026-09-08 13:03:31 +07:00
parent 896cc48128
commit 684ac58b73
8 changed files with 831 additions and 0 deletions
+136
View File
@@ -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.
+10
View File
@@ -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) — функциональная документация.
+101
View File
@@ -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.
+111
View File
@@ -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.
+154
View File
@@ -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`.
+107
View File
@@ -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 и системных ограничений батареи/фоновой работы.
+212
View File
@@ -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 <access-token>
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` и документацию.
Vendored Executable → Regular
View File