added AGENTS.ms and docs for Android app
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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 и системных ограничений батареи/фоновой работы.
|
||||
@@ -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` и документацию.
|
||||
Reference in New Issue
Block a user