feat(proj): init
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# 2026-09-09 — Telegram-бот «от зарплаты до зарплаты»
|
||||
|
||||
## Запрос
|
||||
Сделать бота: ввод суммы и даты «до зарплаты», расчёт дневного лимита, быстрый учёт дневных трат.
|
||||
|
||||
## Решение
|
||||
С нуля собран Telegram-бот на **aiogram 3** + **SQLite (aiosqlite)**.
|
||||
|
||||
### Структура
|
||||
- `bot/main.py` — точка входа, polling
|
||||
- `bot/config.py` — `BOT_TOKEN` из `.env`
|
||||
- `bot/db/` — схема, репозиторий
|
||||
- `bot/services/budget.py` — лимит дня от остатка на утро (`(остаток+траты_сегодня)/дней`), модель «утреннего конверта»
|
||||
- `bot/services/parsing.py` — парсинг сумм/дат/трат
|
||||
- `bot/handlers/budget.py` — команды и FSM настройки бюджета
|
||||
|
||||
### UX
|
||||
- `/budget` → сумма → дата
|
||||
- Трата одним сообщением: `250`, `250 кофе`, `кофе 250`
|
||||
- `/status`, `/today`, `/undo`
|
||||
- Reply-клавиатура: Статус / Сегодня / Новый бюджет / Отмена траты
|
||||
|
||||
### Запуск
|
||||
1. Токен от BotFather в `.env`
|
||||
2. `pip install -r requirements.txt`
|
||||
3. `python -m bot.main`
|
||||
|
||||
## Не сделано (можно следующим шагом)
|
||||
- Категории трат и отчёты за период
|
||||
- Напоминание вечером, если лимит не выбран
|
||||
- Мультивалютность / несколько бюджетов
|
||||
- Деплой (systemd / Docker)
|
||||
@@ -0,0 +1,15 @@
|
||||
# 2026-09-13 — Dockerfile для бота
|
||||
|
||||
## Запрос
|
||||
Написать Dockerfile.
|
||||
|
||||
## Сделано
|
||||
- `Dockerfile` — `python:3.12-slim`, non-root user `bot`, `CMD python -m bot.main`
|
||||
- `.dockerignore` — исключает `.venv`, `.env`, `data/`, кэши
|
||||
- В README добавлен пример `docker build` / `docker run` с volume для SQLite
|
||||
|
||||
## Запуск
|
||||
```bash
|
||||
docker build -t please-pay-me-bot .
|
||||
docker run --rm -e BOT_TOKEN=... -v ./data:/app/data please-pay-me-bot
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# 2026-09-13 — SOCKS5-прокси для бота в Docker
|
||||
|
||||
## Запрос
|
||||
Xray на хосте (10808/10809) слушает 127.0.0.1; контейнер его не видит. Нужно открыть listen на 0.0.0.0 и настроить aiogram через SOCKS5.
|
||||
|
||||
## Сделано в репозитории
|
||||
- `aiohttp-socks` в `requirements.txt`
|
||||
- `PROXY_URL` в `bot/config.py` (опционально)
|
||||
- `bot/main.py` — `AiohttpSession(proxy=...)` (официальный API aiogram 3, без `_connector_init`)
|
||||
- `scripts/open_xray_listen.sh` — правки Xray на Ubuntu-хосте + restart
|
||||
- `docker-compose.yml` — `PROXY_URL=socks5://172.17.0.1:10808` по умолчанию
|
||||
- README: секция прокси + firewall warning
|
||||
|
||||
## На хосте (вручную)
|
||||
```bash
|
||||
./scripts/open_xray_listen.sh
|
||||
# в .env: PROXY_URL=socks5://172.17.0.1:10808
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Конфиг `/usr/local/etc/xray/config.json` с этой Windows-машины недоступен — правки только через скрипт на Ubuntu.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 2026-09-13 — Fix: ProxyTimeout к 172.17.0.1
|
||||
|
||||
## Симптом
|
||||
Контейнер: `Proxy connection timed out` к `socks5://172.17.0.1:10808`.
|
||||
|
||||
## Причина
|
||||
В bridge/Portainer-сети хост `172.17.0.1:10808` часто недоступен (Xray на 127.0.0.1 и/или другая docker-сеть).
|
||||
|
||||
## Исправление
|
||||
- `docker-compose.yml`: `network_mode: host` + `PROXY_URL=socks5://127.0.0.1:10808`
|
||||
- Убран захардкоженный `BOT_TOKEN` из compose (был в логах/файле — нужен revoke у BotFather)
|
||||
- Xray можно оставить на `127.0.0.1` (безопаснее, чем 0.0.0.0)
|
||||
|
||||
## Действия на сервере
|
||||
1. BotFather → revoke/перевыпустить токен
|
||||
2. В `.env`: новый токен + `PROXY_URL=socks5://127.0.0.1:10808`
|
||||
3. `docker compose up -d --build`
|
||||
@@ -0,0 +1,13 @@
|
||||
# 2026-09-13 — Траты за произвольную дату
|
||||
|
||||
## Запрос
|
||||
Возможность добавлять трату за определённое число.
|
||||
|
||||
## Сделано
|
||||
- Парсер: `за 12.09 250 кофе`, `250 кофе за 12.09`, `12.09 250`, `250 кофе 12.09`
|
||||
- Дата без года для трат берёт прошлое/текущее (не будущее), в отличие от даты бюджета
|
||||
- `BudgetService.add_expense(..., spent_at=)` с проверкой периода бюджета
|
||||
- FSM `/spend` и кнопка «📅 За дату»
|
||||
- `/day 12.09` — список трат за дату
|
||||
|
||||
Месяц в дате с точкой лучше писать двузначно (`12.09`), чтобы не путать с суммой `12.5`.
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2026-09-13 — Fix: FSM «За дату» глотал все сообщения
|
||||
|
||||
## Баг
|
||||
После «📅 За дату» состояние `waiting_date` было зарегистрировано раньше `/status`, `/today`, `/day`. Любой ввод (включая кнопки) шёл в парсер даты. Сообщение `12.09 кб` целиком не парсилось как дата → вечный цикл ошибки.
|
||||
|
||||
## Исправление
|
||||
- Команды и кнопки меню регистрируются **до** FSM и сбрасывают state
|
||||
- На шаге даты: one-shot `12.09 250 кб`, либо дата из первого токена (`12.09 кб` → дата + заметка, потом спросить сумму)
|
||||
- Подсказка про `/cancel`
|
||||
|
||||
После деплоя: если бот всё ещё «залип» — один раз `/cancel` или `/help`.
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2026-09-13 — Постраничный просмотр трат периода
|
||||
|
||||
## Запрос
|
||||
Просматривать траты за текущий период бюджета постранично.
|
||||
|
||||
## Сделано
|
||||
- Репозиторий: `count/sum/list_expenses_in_period` с LIMIT/OFFSET
|
||||
- `BudgetService.get_period_expenses_page` (по 8 записей, новые сверху)
|
||||
- `/history` и кнопка «📒 Период»
|
||||
- Inline «‹ Назад» / «N/M» / «Вперёд ›»
|
||||
- Сумма статуса периода тоже считается строго в границах бюджета
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-09-13 — API + веб-кабинет для бюджетов и трат
|
||||
|
||||
## Запрос
|
||||
Сделать API и фронт для просмотра бюджетов и трат.
|
||||
|
||||
## Сделано
|
||||
### API (`api/`)
|
||||
- FastAPI, тот же SQLite (`data/budget.db`, WAL)
|
||||
- `GET /api/health` без токена
|
||||
- `GET /api/budgets`, `/api/budgets/{user_id}`, `/api/budgets/{user_id}/expenses`
|
||||
- Auth: `X-API-Token` / Bearer `API_TOKEN`
|
||||
|
||||
### Web (`web/`)
|
||||
- React + Vite + React Router
|
||||
- Список бюджетов, деталка со статусом и постраничными тратами
|
||||
- Токен в localStorage
|
||||
|
||||
### Docker
|
||||
- `Dockerfile.api` (multi-stage: npm build + uvicorn)
|
||||
- `docker-compose.yml`: сервисы `bot` + `web` (:8000)
|
||||
|
||||
## Запуск
|
||||
```bash
|
||||
# .env: API_TOKEN=...
|
||||
docker compose up -d --build
|
||||
# кабинет: http://HOST:8000
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# 2026-09-13 — Уточнение docker-compose: BOT_TOKEN vs API_TOKEN
|
||||
|
||||
## Вопрос
|
||||
Почему у bot убран токен и зачем фронту токен бота?
|
||||
|
||||
## Ответ
|
||||
1. **BOT_TOKEN не убирался у бота** — убрали только хардкод из compose (токен светился в логах). Бот читал его из `.env` через `env_file`. Сейчас явно: `BOT_TOKEN: ${BOT_TOKEN}`.
|
||||
2. **Фронту BOT_TOKEN не нужен.** Нужен отдельный `API_TOKEN` для защиты read-only API кабинета. У сервиса `web` убран `env_file`, чтобы Telegram-токен туда вообще не попадал.
|
||||
@@ -0,0 +1,10 @@
|
||||
# 2026-09-13 — Разделение портов фронт/API
|
||||
|
||||
## Запрос
|
||||
Фронт на внешнем порту 51290, API на 51291; фронт ходит на localhost:51291.
|
||||
|
||||
## Сделано
|
||||
- `api` сервис: `51291:8000`, только FastAPI (без статики)
|
||||
- `web` сервис: nginx `51290:80`, `VITE_API_BASE_URL=http://localhost:51291`
|
||||
- CORS по умолчанию: `http://localhost:51290`, `http://127.0.0.1:51290`
|
||||
- `web/src/api.ts` собирает абсолютные URL к API
|
||||
@@ -0,0 +1,4 @@
|
||||
# 2026-09-13 — Красивая 404
|
||||
|
||||
Стилизована под кабинет (Syne/Manrope, зелёный градиент, motion).
|
||||
Файлы: корневой `404.html` + `web/public/404.html`, nginx `error_page 404`.
|
||||
@@ -0,0 +1,4 @@
|
||||
# 2026-09-13 — Nginx proxy /api → API
|
||||
|
||||
Фронт ходит на same-origin `/api`. Nginx проксирует на `http://api:8000` (сервис compose = хост `localhost:51291`).
|
||||
`VITE_API_BASE_URL` по умолчанию пустой. Vite dev proxy → `http://localhost:51291`.
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2026-09-13 — Fix: API readonly database
|
||||
|
||||
## Ошибка
|
||||
`sqlite3.OperationalError: attempt to write a readonly database` на `PRAGMA journal_mode=WAL`.
|
||||
|
||||
## Причина
|
||||
API (uid 1001) и бот (uid 1000) делили `./data`; WAL/схема требуют запись.
|
||||
|
||||
## Исправление
|
||||
- `Database(read_only=True)` для API: `file:...?mode=ro`, без WAL и SCHEMA
|
||||
- `Dockerfile.api` — uid 1000 как у бота
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2026-09-13 — Fix: JSON.parse на фронте
|
||||
|
||||
## Симптом
|
||||
`JSON.parse: unexpected character at line 1 column 1` — в ответ на `/api/budgets` приходил HTML `index.html` (SPA fallback).
|
||||
|
||||
## Причина
|
||||
В `web/nginx.conf` не было `location /api/` → `try_files` отдавал фронт.
|
||||
|
||||
## Исправление
|
||||
Вернул `proxy_pass http://api:8000` для `/api/`, улучшил разбор ошибок во фронте.
|
||||
Пересобрать: `docker compose up -d --build web`
|
||||
@@ -0,0 +1,10 @@
|
||||
# 2026-09-13 — Личный кабинет через Telegram Login
|
||||
|
||||
## Сделано
|
||||
- POST `/api/auth/telegram` — проверка Login Widget + JWT
|
||||
- GET `/api/me`, `/api/me/budget`, `/api/me/expenses` — только свой user_id
|
||||
- Фронт: кнопка Telegram Login, кабинет своего бюджета
|
||||
- Env: `BOT_TOKEN`, `TELEGRAM_BOT_USERNAME` (+ опционально `API_TOKEN` для админских `/api/budgets`)
|
||||
|
||||
## BotFather
|
||||
`/setdomain` на домен кабинета (нужен публичный HTTPS).
|
||||
@@ -0,0 +1,13 @@
|
||||
# 2026-09-13 — Write API + формы трат/бюджета в кабинете
|
||||
|
||||
## Сделано
|
||||
- API: `read_only=False`
|
||||
- `POST /api/me/expenses`, `DELETE /api/me/expenses/last`, `PUT /api/me/budget`
|
||||
- Фронт: формы «Добавить трату» и «Задать/обновить бюджет»
|
||||
- CORS: PUT/DELETE
|
||||
|
||||
## На сервере
|
||||
```bash
|
||||
sudo chown -R 1000:1000 ./data
|
||||
docker compose up -d --build api web
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# 2026-09-13 — Редизайн web-кабинета
|
||||
|
||||
## UX
|
||||
- Login: brand-first, один CTA (Telegram)
|
||||
- Кабинет: hero = остаток; быстрая трата; история; настройки бюджета в `<details>`
|
||||
- Flash с aria-live, skeleton, focus-visible, reduced-motion
|
||||
- Крупные touch-target (48px), sticky topbar
|
||||
|
||||
Сохранить палитру леса/зелени проекта. Пересборка: `docker compose up -d --build web`
|
||||
@@ -0,0 +1,4 @@
|
||||
# 2026-09-13 — Строгий системный UI кабинета
|
||||
|
||||
Светлая тема, IBM Plex Sans/Mono, шкала отступов 4–48px, hairline-границы,
|
||||
без glow/градиентов. Суммы в mono. Секции: Обзор → Операция → Журнал → Параметры.
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-09-13 — Системный продуктовый дизайн (web)
|
||||
|
||||
Заложена расширяемая дизайн-система кабинета вместо монолита `App.tsx`.
|
||||
|
||||
## Структура
|
||||
|
||||
- `web/src/design/` — токены (`tokens.css`), base, стили shell/UI
|
||||
- `web/src/components/ui/` — Button, Field, Flash, ProgressBar, MetricGrid, PageHeader, Section, EmptyState, Banner
|
||||
- `web/src/components/layout/` — AppShell, AuthLayout, `PRIMARY_NAV` (точка расширения меню)
|
||||
- `web/src/auth/` — AuthProvider, RequireAuth
|
||||
- `web/src/cabinet/` — CabinetProvider (бюджет/траты), CabinetLayout
|
||||
- `web/src/pages/` — Login, Overview, Operations, Journal, Period (+ ComingSoon заготовка)
|
||||
|
||||
## Маршруты
|
||||
|
||||
`/login` · `/` · `/operations` · `/journal` · `/period`
|
||||
В nav зарезервированы «Отчёты» и «Настройки» (`soon: true`).
|
||||
|
||||
## Как добавлять фичу
|
||||
|
||||
1. Пункт в `PRIMARY_NAV`
|
||||
2. Route под `CabinetLayout` в `App.tsx`
|
||||
3. Страница в `pages/`, UI из `components/ui`
|
||||
4. Доменное состояние — в `cabinet/` или новый context
|
||||
|
||||
Сборка `npm run build` проходит успешно.
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-09-13 — Бот ходит в БД только через API
|
||||
|
||||
## Цель
|
||||
Единый путь записи/чтения: web и Telegram-бот → FastAPI → SQLite.
|
||||
Бот больше не монтирует `./data` и не открывает `budget.db`.
|
||||
|
||||
## API
|
||||
- `POST /api/auth/internal` + `X-API-Token` / `API_TOKEN` → JWT как у web
|
||||
- Бот вызывает те же `/api/me/budget`, `/api/me/expenses`, undo, upsert
|
||||
- `GET /api/me/expenses?spent_at=YYYY-MM-DD` — траты за день (для /today, /day)
|
||||
|
||||
## Bot
|
||||
- `bot/clients/budget_api.py` — `BudgetApiClient` (aiohttp)
|
||||
- `bot/config.py`: `API_BASE_URL`, `API_TOKEN` (обязателен)
|
||||
- Compose: bot без volume data; `API_BASE_URL=http://127.0.0.1:51291` при host network
|
||||
|
||||
## Деплой
|
||||
Пересобрать `api` + `bot`, убедиться что `API_TOKEN` одинаковый в обоих сервисах.
|
||||
@@ -0,0 +1,23 @@
|
||||
# 2026-09-13 — Несколько бюджетов + is_active
|
||||
|
||||
## Модель
|
||||
- У пользователя много строк в `budgets` (снят UNIQUE user_id)
|
||||
- Поля: `name`, `is_active`, `users.selected_budget_id`
|
||||
- `expenses.budget_id` — траты привязаны к конкретному бюджету
|
||||
- Миграция на старте API (`migrate_schema`) для старых БД
|
||||
|
||||
## Семантика
|
||||
- **Активный** — можно писать траты
|
||||
- **Текущий (selected)** — default для /status, операций, журнала
|
||||
- Неактивный отклоняет новые expenses
|
||||
|
||||
## API
|
||||
- `GET/POST /api/me/budgets`
|
||||
- `PUT /api/me/budgets/{id}`
|
||||
- `PATCH /api/me/budgets/{id}/active`
|
||||
- `POST /api/me/budgets/{id}/select`
|
||||
- `budget_id` на expenses / budget GET
|
||||
|
||||
## Клиенты
|
||||
- Web: страница `/budgets`, переключатель вкл/выкл и «текущий»
|
||||
- Bot: `/budgets`, `/budget` создаёт новый; inline select/toggle
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-09-13 — API на C# + PostgreSQL
|
||||
|
||||
## Что сделано
|
||||
- Новый стек: `src/PleasePayMe.*` (Domain / Application / Infrastructure / Api)
|
||||
- EF Core + Npgsql, миграция `InitialCreate`, авто-`Migrate()` на старте
|
||||
- HTTP-контракт прежний (`/api/auth/*`, `/api/me/*`, `/api/budgets/*`, snake_case JSON, `{detail}`)
|
||||
- Docker: сервис `db` (postgres:16), `Dockerfile.api` → ASP.NET
|
||||
- Python `api/` помечен deprecated; бот и web без смены контракта
|
||||
|
||||
## Запуск
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
Переменные: `BOT_TOKEN`, `API_TOKEN`, `POSTGRES_*`.
|
||||
|
||||
## Данные
|
||||
SQLite `data/budget.db` в compose больше не используется.
|
||||
Перенос старых данных — отдельной задачей (не автоматический).
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-13 — Удаление бюджетов
|
||||
|
||||
- `DELETE /api/me/budgets/{id}` → 204; траты каскадом; selected переназначается
|
||||
- Web: кнопка «Удалить» с confirm на странице Бюджеты
|
||||
- Bot: кнопка 🗑 в `/budgets`
|
||||
@@ -0,0 +1,14 @@
|
||||
# 2026-09-13 — Раздел «Работа»
|
||||
|
||||
## Модель
|
||||
- Таблица `jobs`: name, salary_amount, pay_days (integer[] 1–31), is_active
|
||||
- Если дня нет в месяце (напр. 31) — берётся последний день месяца
|
||||
- API считает `next_pay_dates` (ближайшие выплаты)
|
||||
|
||||
## API
|
||||
- `GET/POST /api/me/jobs`
|
||||
- `PUT/DELETE /api/me/jobs/{id}`
|
||||
|
||||
## Web
|
||||
- Навигация «Работа» → `/work`
|
||||
- Список + форма: название, ЗП, дни (текст `5, 20` + быстрые чипы)
|
||||
@@ -0,0 +1,6 @@
|
||||
# 2026-09-13 — Работа: доля выплат + сдвиг с выходных
|
||||
|
||||
- Максимум 2 дня в `pay_days`
|
||||
- `first_pay_percent` — доля более раннего дня; второй = 100 − first
|
||||
- `weekend_policy`: `before_weekend` | `after_weekend` (сб/вс → пт или пн)
|
||||
- `next_pays[]`: дата (уже с учётом сдвига), scheduled_day, percent, amount
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-13 — Выбор даты начала бюджета
|
||||
|
||||
- API уже принимал `start_date` (create/update/upsert); upsert теперь пробрасывает дату.
|
||||
- Web: поле «С даты» в Бюджеты и Период; по умолчанию `todayIso()` при создании.
|
||||
- Bot client: `create_budget` / `set_budget` передают `start_date`, если задан (иначе API = сегодня).
|
||||
@@ -0,0 +1,6 @@
|
||||
# 2026-09-13 — Календарь бюджетов и выплат
|
||||
|
||||
- Маршрут `/calendar`, пункт «Календарь» в nav
|
||||
- Полоски цвета = периоды бюджетов (стабильный цвет по id)
|
||||
- Кружки = дни ЗП (с учётом weekend_policy, как в PaySchedule)
|
||||
- Клик по дню — детали; легенда ниже сетки
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-13 — Календарь 2.0: траты на днях
|
||||
|
||||
- API `GET /api/me/expenses/range?from=&to=` — траты по всем бюджетам пользователя (≤93 дня)
|
||||
- Ячейка: точка + компактная сумма трат за день
|
||||
- Клик по дню → блок «Операции» (сумма, заметка, бюджет) + контекст (бюджеты/ЗП)
|
||||
@@ -0,0 +1,6 @@
|
||||
# 2026-09-19 — Траты без активного бюджета + журнал «все»
|
||||
|
||||
- `AddExpense`: больше не требует `is_active`; для неактивного бюджета даты периода не ограничивают
|
||||
- `GET /api/me/expenses?all=true` — пагинация по всем бюджетам пользователя (`budget_id: null`)
|
||||
- Журнал: переключатель **Все / Текущий** (по умолчанию все); в режиме «все» видно имя бюджета
|
||||
- Операции: выбор бюджета в форме, в т.ч. выключенного
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-09-19 — Flutter mobile + portable Widgetbook
|
||||
|
||||
## Mobile app (`mobile/`)
|
||||
- Flutter scaffold (android/ios/web/windows)
|
||||
- Theme tokens aligned with web `tokens.css` + light/dark `ThemeData`
|
||||
- UI kit: atoms / molecules / navigation / feedback / sample screens
|
||||
- IBM Plex Sans via `google_fonts`
|
||||
|
||||
## Widgetbook (`mobile/widgetbook/`)
|
||||
- Separate package (`please_pay_me_widgetbook`) path-depends on app
|
||||
- Catalog: Atoms, Molecules, Navigation, Feedback, Screens + knobs
|
||||
- Addons: MaterialTheme (light/dark), Localization (ru/en), Viewport (iPhone/Android)
|
||||
- Smoke tests: `flutter test` — all use-cases registered
|
||||
|
||||
## Run
|
||||
```bash
|
||||
cd mobile/widgetbook && flutter pub get && flutter run -d chrome
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-19 — PS1 скрипты запуска mobile/widgetbook
|
||||
|
||||
- `mobile/run.ps1` — app или `-Target widgetbook`
|
||||
- `mobile/widgetbook/run.ps1` — каталог UI (`-Device chrome|windows|edge`)
|
||||
- Ищут Flutter в PATH и `%USERPROFILE%\flutter\bin`
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-19 — Widgetbook: добавлен Windows desktop support
|
||||
|
||||
- `flutter create . --platforms=windows,web,android,ios` в `mobile/widgetbook`
|
||||
- Исправлена заглушка `test/widget_test.dart` после create
|
||||
- Повтор: `.\run.ps1 -Device windows`
|
||||
@@ -0,0 +1,66 @@
|
||||
# 2026-09-19 — Мобильный UI-кит переведён на iOS (Cupertino)
|
||||
|
||||
## Запрос
|
||||
Widgetbook запустился, но Material-компоненты визуально не устроили. Задача — сделать
|
||||
компоненты «из iOS kit».
|
||||
|
||||
## Что сделано
|
||||
|
||||
### Токены и тема (`mobile/lib/theme/`)
|
||||
- `tokens.dart` переписан под Apple HIG: семантические цвета (`label`, `secondaryLabel`,
|
||||
`separator`, `opaqueSeparator`, `groupedBackground`, `groupedSurface`, `barBackground`)
|
||||
и системная палитра (`systemRed/Orange/Green/Gray/Gray3/Gray5/Gray6`) объявлены как
|
||||
`CupertinoDynamicColor.withBrightness` — light/dark резолвится самим фреймворком через
|
||||
`AppColors.of(context, color)`.
|
||||
- Бренд-тинт остался зелёным (`accent` = #12885A / #3CD68C в тёмной) и подменяет systemBlue.
|
||||
- Типографика — шкала SF Pro: largeTitle 34 … caption2 11 с нативными letterSpacing.
|
||||
- Размеры контролов: 50 / 44 / 34 pt, hairline 0.5, радиусы 6/10/12/16/capsule.
|
||||
- `app_theme.dart` теперь возвращает `CupertinoThemeData` (`buildLightTheme` /
|
||||
`buildDarkTheme`), шрифт — Inter через google_fonts как кросс-платформенная замена SF Pro
|
||||
(на Windows/web SF недоступен).
|
||||
|
||||
### Компоненты (`mobile/lib/ui/`)
|
||||
Material-версии удалены, вместо них Cupertino:
|
||||
- atoms: `AppButton` (filled/tinted/gray/plain/destructive × large/medium/small),
|
||||
`AppTextField` (CupertinoTextField + error/prefix/clear), `AppText` (именованные
|
||||
конструкторы по шкале iOS) + `AppSectionHeader`, `AppIcon` + `AppIconBadge`
|
||||
(Settings-style скруглённый квадрат).
|
||||
- molecules: `AppListSection` (inset-grouped, header/footer, hairline с indent),
|
||||
`AppListTile` (leading/value/chevron, press-highlight, destructive), `AppSwitchRow`
|
||||
(CupertinoSwitch), `AppCard`, `AppChip` (капсула), `AppAvatar`.
|
||||
- navigation: `AppNavBar` (реализует `ObstructingPreferredSizeWidget`, есть subtitle),
|
||||
`AppLargeNavBar` (`CupertinoSliverNavigationBar`), `AppTabBar` (наследник
|
||||
`CupertinoTabBar`, чтобы отдавать в `CupertinoTabScaffold`), `AppSegmentedControl`
|
||||
(`CupertinoSlidingSegmentedControl`), `AppAlert` + `showAppAlert` + `showAppActionSheet`.
|
||||
- feedback: `AppSpinner` (`CupertinoActivityIndicator`), `AppProgressBar` (4pt капсула),
|
||||
`AppSkeleton` / `AppSkeletonRow`, `AppToast` + `showAppToast` (blur-капсула через
|
||||
OverlayEntry — в iOS нет SnackBar).
|
||||
- screens: Feed (large title + сегменты + группы + empty/loading), Detail (hero-сумма,
|
||||
прогресс, группа параметров, кнопки), Profile (Settings-подобный экран со свитчами).
|
||||
- `main.dart` переведён на `CupertinoApp` + `CupertinoTabScaffold`.
|
||||
|
||||
### Widgetbook (`mobile/widgetbook/`)
|
||||
- `Widgetbook.cupertino` + `CupertinoThemeAddon` вместо material-варианта.
|
||||
- Новый `lib/support/preview.dart` — `IosPreview` кладёт use-case на grouped-фон,
|
||||
чтобы карточки и разделители читались как в приложении.
|
||||
- Каталог: Atoms (Button/TextField/Typography/Icons), Molecules (Card/ListTile/SwitchRow/
|
||||
Chip/Avatar), Navigation (NavBar/TabBar/SegmentedControl/Alert), Feedback (Progress/
|
||||
Skeleton/Toast), Screens (Feed/Detail/Profile) — 42 use-case.
|
||||
- Knobs: добавлены `knobEnum` и `knobTint` (дропдаун по системным тинтам), убран
|
||||
deprecated `knobs.list`.
|
||||
|
||||
### Тесты
|
||||
`widgetbook/test/catalog_smoke_test.dart` больше не проверяет только структуру: каждый
|
||||
use-case пампится в light и dark внутри `WidgetbookScope` (knobs читают `WidgetbookState`
|
||||
из контекста, иначе падает «No Widgetbook found in the context»). В тестах отключён
|
||||
`GoogleFonts.config.allowRuntimeFetching` и вызван `TestWidgetsFlutterBinding.ensureInitialized()`.
|
||||
|
||||
## Результат
|
||||
- `flutter analyze` (mobile + widgetbook) — 0 issues.
|
||||
- `flutter test` — 84 теста в widgetbook + 1 в приложении, всё зелёное.
|
||||
|
||||
## Запуск
|
||||
```powershell
|
||||
cd mobile\widgetbook
|
||||
.\run.ps1 -Device windows
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# 2026-09-20 — Мобильное приложение Please Pay Me собрано на iOS-ките
|
||||
|
||||
## Запрос
|
||||
«Собери теперь приложение please-pay-me, используя эти компоненты» — то есть поверх
|
||||
Cupertino-кита из прошлой сессии сделать рабочее приложение против существующего API.
|
||||
|
||||
## Что сделано
|
||||
|
||||
### Слой данных (`lib/data/`)
|
||||
- `models/` — `Budget`, `BudgetStatus` (+ вычисляемые `spentProgress` / `dailyProgress`),
|
||||
`Expense`, `ExpensesPage`, `Job`, `UpcomingPay`, `WeekendPolicy`, `AuthUser`, `AuthSession`.
|
||||
Зеркалят `web/src/types.ts`, парсятся через `models/json.dart` — толерантные
|
||||
коэрсеры (`int`/`double`/`string`), потому что API отдаёт числа из C#-рекордов.
|
||||
- `api/api_client.dart` — транспорт на `http`: Bearer-токен через `tokenProvider`,
|
||||
маппинг `detail`/`title` в `ApiException`, отдельная обработка 401 с колбэком
|
||||
`onUnauthorized` (разлогинивает сессию).
|
||||
- `repositories/` — интерфейсы `BudgetRepository` / `ExpenseRepository` /
|
||||
`JobRepository` / `UserRepository` + REST-реализации на все ручки `MeController`.
|
||||
- `demo/demo_backend.dart` — in-memory бэкенд, повторяющий математику конверта
|
||||
(`dailyLimit = remaining / daysLeft`). Используется в демо-режиме, превью и тестах;
|
||||
`DemoBackend.empty()` — для пустых состояний.
|
||||
|
||||
### Ядро (`lib/core/`)
|
||||
- `config/app_config.dart` — `PPM_API_BASE_URL`, `PPM_WEB_URL`, `PPM_DEMO` через
|
||||
`--dart-define`; пустой адрес API автоматически включает демо-режим.
|
||||
- `storage/session_storage.dart` — интерфейс + `SharedPreferences` и in-memory реализации.
|
||||
- `state/async_value.dart` — sealed `AsyncLoading/AsyncData/AsyncError` с `map(...)`,
|
||||
чтобы экраны матчились по состоянию, а не жонглировали тремя nullable-полями.
|
||||
- `format/formatters.dart` — деньги (`1 234,50 ₽`), русские плюрали, «Сегодня/Вчера».
|
||||
|
||||
### Состояние (`lib/features/*/`)
|
||||
- `SessionController` — владеет авторизацией и выдаёт репозитории (REST или demo),
|
||||
так что остальное приложение не знает про токены. `sessionKey` пересобирает
|
||||
фичевые контроллеры при входе/выходе.
|
||||
- `BudgetsController` — источник правды по конвертам (список, выбранный, мутации
|
||||
с авто-перезагрузкой, возврат текста ошибки вместо исключения).
|
||||
- `JournalController` — пагинация + группировка по дням + скоуп «текущий/все».
|
||||
- `JobsController` — работы и ближайшая выплата.
|
||||
|
||||
### Экраны (`lib/features/`, `lib/app/`)
|
||||
Пять вкладок: Обзор, Журнал, Бюджеты, Работа, Профиль (+ экран входа и сплэш).
|
||||
Формы — модальные шиты: новая/редактирование траты, бюджета, работы; все они
|
||||
принимают `onSubmit`, возвращающий текст ошибки, и не знают про репозитории.
|
||||
`_SessionGate` разводит `restoring / signedOut / signedIn`.
|
||||
|
||||
### Кит
|
||||
Удалены демо-экраны `lib/ui/screens/*`; добавлены `AppEmptyState`, `AppErrorView`,
|
||||
`AppLoadingView`, `showAppDatePicker`, `showAppFormSheet`.
|
||||
|
||||
### Widgetbook
|
||||
Папка Screens теперь показывает **настоящие экраны** приложения на `DemoScope`
|
||||
(demo-бэкенд + провайдеры), включая пустые состояния, формы и полный таб-бар.
|
||||
|
||||
### Авторизация — известное ограничение
|
||||
Telegram Login Widget работает только в вебе. Мобильный клиент принимает JWT из
|
||||
`localStorage.ppm_session_jwt` веб-кабинета и валидирует его через `GET /api/me`.
|
||||
Нативный вход (WebView с Telegram-виджетом либо one-time code через бота)
|
||||
не делался — это отдельное продуктовое решение.
|
||||
|
||||
## Тесты
|
||||
- `mobile`: 32 теста — парсинг моделей, форматтеры, все контроллеры на demo-бэкенде,
|
||||
виджет-сценарии (вход, табы, пустые состояния, запись траты end-to-end).
|
||||
- `mobile/widgetbook`: 100 — каждый use-case рендерится в light и dark.
|
||||
- `flutter analyze` по обоим пакетам — 0 issues.
|
||||
- `flutter build windows --debug` проходит (проверка плагинов shared_preferences/url_launcher).
|
||||
|
||||
## Запуск
|
||||
```powershell
|
||||
cd mobile
|
||||
.\run.ps1 # демо-данные
|
||||
.\run.ps1 -ApiBaseUrl https://ppm.example.com
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# 2026-09-20 — Вход через WebView + Telegram Login Widget
|
||||
|
||||
## Запрос
|
||||
Реализовать нативный вход: открыть кабинет в WebView и забрать JWT через
|
||||
JS-канал. Бэкенд не менять.
|
||||
|
||||
## Почему не «просто открыть страницу»
|
||||
|
||||
Telegram Login Widget в обычном браузере работает через iframe + `data-onauth`.
|
||||
В Android/iOS WebView этот путь ломается:
|
||||
|
||||
- popup (`window.open` на `oauth.telegram.org`) не возвращает `opener`;
|
||||
- iframe режется third-party cookies и User-Agent с маркером `; wv`;
|
||||
- callback `onTelegramAuth` просто не вызывается.
|
||||
|
||||
Поэтому в WebView виджет переключается на **redirect-режим** (`data-auth-url`),
|
||||
а Flutter забирает уже готовый JWT, который кабинет и так кладёт в
|
||||
`localStorage.ppm_session_jwt`. API (`POST /api/auth/telegram`) не трогали.
|
||||
|
||||
## Поток
|
||||
|
||||
1. Экран входа на iOS/Android показывает «Войти через Telegram».
|
||||
2. Открывается fullscreen WebView на `{cabinet}/login`.
|
||||
3. Кабинет видит канал `window.PpmAuth` → ставит виджету `data-auth-url=/login`.
|
||||
4. Пользователь логинится в Telegram; редирект приходит на `/login?id&hash&…`.
|
||||
5. `telegramPayloadFromQuery` собирает payload, кабинет зовёт тот же
|
||||
`loginWithTelegram` / `POST /api/auth/telegram`.
|
||||
6. `setSession` пишет JWT в `localStorage` и зовёт `PpmAuth.postMessage`.
|
||||
7. Flutter парсит сообщение, закрывает WebView, проверяет токен через
|
||||
`GET /api/me`, кладёт сессию в `SharedPreferences`.
|
||||
|
||||
Запасные пути, если кабинет ещё не задеплоен с мостом:
|
||||
|
||||
- JS в WebView хукает `localStorage.setItem('ppm_session_jwt')` и поллит ключ;
|
||||
- на Windows/web WebView нет — форма «вставить токен»;
|
||||
- «Демо-режим» по-прежнему без сети.
|
||||
|
||||
## Что изменилось
|
||||
|
||||
### Web (не API)
|
||||
- `web/src/auth/telegramRedirect.ts` — разбор query и `postTokenToNativeApp`.
|
||||
- `TelegramLoginButton` — `data-auth-url` только если есть `window.PpmAuth`.
|
||||
- `LoginPage` — автологин, если в URL уже лежит telegram-payload.
|
||||
- `setSession` — после записи токена шлёт его в канал.
|
||||
|
||||
### Mobile
|
||||
- `TelegramLoginScreen` (`webview_flutter`): канал `PpmAuth`, Chrome UA без
|
||||
`; wv`, third-party cookies на Android, `tg://` уходит во внешнее приложение,
|
||||
кнопка «назад» после oauth.telegram.org.
|
||||
- `resolveCabinetLoginUri` всегда открывает `/login`.
|
||||
- `INTERNET` в main `AndroidManifest` (раньше был только в debug).
|
||||
- iOS: `LSApplicationQueriesSchemes` для `tg` / `telegram`.
|
||||
- `PPM_WEB_URL` по умолчанию = `PPM_API_BASE_URL` (один origin за nginx).
|
||||
|
||||
## Тесты
|
||||
- `resolveApiBaseUrl` / `resolveCabinetLoginUri` / парсер канала.
|
||||
- виджет: инжектированный launcher отдаёт JWT → `GET /api/me` → signed in.
|
||||
- 32 теста приложения, 102 в Widgetbook, `flutter analyze` чистый.
|
||||
|
||||
## Запуск против живого кабинета
|
||||
|
||||
```powershell
|
||||
cd mobile
|
||||
.\run.ps1 -Device windows # на Windows будет форма токена
|
||||
# на телефоне:
|
||||
.\run.ps1 -ApiBaseUrl https://<домен-кабинета>
|
||||
```
|
||||
|
||||
Нужен публичный HTTPS и BotFather `/setdomain` на этот домен — как для веба.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-09-20 — API URL мобильного приложения из `.env`
|
||||
|
||||
## Запрос
|
||||
Задавать `ApiBaseUrl` через переменные `.env`, а не флагом `run.ps1`.
|
||||
|
||||
## Решение
|
||||
Конфиг читается из `mobile/.env` и уходит во Flutter как
|
||||
`--dart-define-from-file` — без `flutter_dotenv` в ассетах (иначе gitignored
|
||||
`.env` ломал бы CI: asset обязан существовать в момент сборки).
|
||||
|
||||
Порядок в `AppConfig.fromEnvironment`:
|
||||
1. `--dart-define` (в т.ч. из файла)
|
||||
2. явно переданная map (тесты)
|
||||
3. пустой URL → демо-режим
|
||||
|
||||
`run.ps1`: если `.env` нет — копирует `.env.example`. `-ApiBaseUrl` / `-WebUrl`
|
||||
остались как разовый override поверх файла. Скрипт сохранён в UTF-8 с BOM и
|
||||
без вложенных кавычек — иначе PowerShell 5.1 на Windows-1251 сыпется
|
||||
`TerminatorExpectedAtEndOfString`.
|
||||
|
||||
Ключи: `PPM_API_BASE_URL`, `PPM_WEB_URL`, `PPM_DEMO`.
|
||||
Локальный `.env` в gitignore (есть и в корневом).
|
||||
@@ -0,0 +1,37 @@
|
||||
# 2026-09-20 — Вход через Яндекс ID (web + mobile)
|
||||
|
||||
## Запрос
|
||||
Кнопка «Войти через Яндекс» в кабинете и в приложении. Client ID / secret
|
||||
уже заведены в Yandex OAuth.
|
||||
|
||||
## Решение
|
||||
Authorization code. Secret только на API (`YANDEX_CLIENT_SECRET` в `.env`,
|
||||
не в web/mobile и не в git). Клиенты получают `client_id` с
|
||||
`GET /api/auth/providers` и шлют `code` + `redirect_uri` в
|
||||
`POST /api/auth/yandex`.
|
||||
|
||||
API меняет код на токен Яндекса (`oauth.yandex.ru/token`), читает профиль
|
||||
(`login.yandex.ru/info`) и выдаёт тот же JWT, что и Telegram.
|
||||
|
||||
`users.user_id` для Яндекса: ` (1 << 50) | yandexId ` — не пересекается с
|
||||
Telegram id и остаётся внутри `Number.MAX_SAFE_INTEGER`, чтобы JSON в
|
||||
браузере и Flutter не терял точность.
|
||||
|
||||
`redirect_uri` сверяется с allowlist (продакшен `https://please-pay-me.ru/`,
|
||||
localhost:51290 и :5173, плюс `YANDEX_REDIRECT_URI` / `YANDEX_REDIRECT_URIS`).
|
||||
|
||||
## Потоки
|
||||
|
||||
- Web: кнопка → `oauth.yandex.ru/authorize` → возврат на `/?code=`
|
||||
(как в кабинете Яндекса) → `RequireAuth` переносит query на `/login` →
|
||||
`loginWithYandex` → `setSession`.
|
||||
- Mobile: своя кнопка → WebView на Яндекс → перехват `{кабинет}/?code=`
|
||||
→ `POST /api/auth/yandex` из Flutter.
|
||||
|
||||
## Что нужно в консоли Яндекса
|
||||
Redirect URI: `https://please-pay-me.ru/` — как зарегистрировано в Яндексе.
|
||||
|
||||
## Секрет
|
||||
Значения записаны в корневой `.env` (gitignore). В репозиторий не коммитятся.
|
||||
Секрет светился в чате — если репозиторий общий, лучше перевыпустить в
|
||||
кабинете Яндекса.
|
||||
@@ -0,0 +1,8 @@
|
||||
# 2026-09-20 — Страница 502
|
||||
|
||||
По аналогии с `404.html`: тот же тёмный градиент, Syne/Manrope, motion.
|
||||
Текст про недоступный шлюз, кнопки «Обновить» и «На главную».
|
||||
|
||||
Файлы: корневой `502.html` + `web/public/502.html`.
|
||||
nginx: `error_page 502 503 504 /502.html` — 503/504 на ту же страницу,
|
||||
потому что при мёртвом API приходит не только 502.
|
||||
@@ -0,0 +1,8 @@
|
||||
# 2026-09-20 — Yandex redirect_uri = корень сайта
|
||||
|
||||
В кабинете Яндекса Callback URL: `https://please-pay-me.ru/` (не `/login`).
|
||||
Клиенты слали `/login` → Яндекс отвечал «redirect_uri не совпадает».
|
||||
|
||||
Теперь authorize и обмен кода используют `https://please-pay-me.ru/`.
|
||||
Возврат `/?code=` RequireAuth переносит на `/login?code=`, чтобы SPA
|
||||
не выкинула query. Mobile перехватывает тот же корень.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-09-20 — Web UI-кит как в mobile iOS kit
|
||||
|
||||
Кабинет переведён на те же токены и примитивы, что Flutter `mobile/lib/ui`.
|
||||
|
||||
## Токены
|
||||
`web/src/design/tokens.css` — Apple HIG: accent #12885A / #3CD68C, label /
|
||||
separator / grouped surfaces, SF-шкала, 4pt сетка, радиусы 6/10/12/16.
|
||||
Светлая и тёмная тема через `prefers-color-scheme`. Шрифт Inter.
|
||||
Старые `--color-*` оставлены как алиасы.
|
||||
|
||||
## Кит
|
||||
`web/src/ui/` — AppButton (filled/tinted/gray/plain/destructive), AppText,
|
||||
AppTextField, AppListSection/Tile, AppSegmented, AppProgress, AppEmpty/Error,
|
||||
Chip, Avatar, Skeleton. Стили в `kit.css`.
|
||||
|
||||
Старые `components/ui` (Button, Field, PageHeader, MetricGrid, …) — тонкие
|
||||
обёртки над китом, страницы не ломаются.
|
||||
|
||||
## Оболочка
|
||||
AppShell: translucent bar, inset-grouped нав на десктопе, капсулы на узком
|
||||
экране. Обзор и журнал собраны на list/segmented. Остальные экраны получают
|
||||
iOS-вид через те же кнопки, поля и `data-list`.
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-20 — Адрес кабинета только из `.env`
|
||||
|
||||
С экрана входа убрано поле «Адрес кабинета». Origin берётся из
|
||||
`PPM_WEB_URL` / `PPM_API_BASE_URL` (`https://please-pay-me.ru/`).
|
||||
Поле токена осталось только в запасном режиме.
|
||||
@@ -0,0 +1,11 @@
|
||||
# 2026-09-20 — Почему «Яндекс недоступен»
|
||||
|
||||
Тост шёл из `_yandex == null`: `GET /api/auth/providers` не вызывался,
|
||||
потому что `AppConfig.fromEnvironment()` читает только `--dart-define`.
|
||||
Запуск из IDE без `run.ps1` оставлял URL пустым, ошибку глотали.
|
||||
|
||||
На проде `/api/auth/providers` уже отдаёт `enabled: true`.
|
||||
|
||||
Фикс: `main()` читает `mobile/.env` с диска (dart-define по-прежнему
|
||||
главнее). По тапу провайдеры перезапрашиваются, в тосте — реальная
|
||||
причина. На Windows WebView нет — отдельное сообщение.
|
||||
@@ -0,0 +1,4 @@
|
||||
# 2026-09-20 — Дефолт кабинета
|
||||
|
||||
Если `PPM_API_BASE_URL` / `PPM_WEB_URL` не заданы, `AppConfig` берёт
|
||||
`https://please-pay-me.ru`. Демо только при `PPM_DEMO=true`.
|
||||
@@ -0,0 +1,6 @@
|
||||
# 2026-09-20 — Переключатель темы в профиле
|
||||
|
||||
На экране профиля: Системная / Светлая / Тёмная (`AppSegmentedControl`).
|
||||
`ThemeController` хранит выбор в SharedPreferences (`ppm_theme_preference`),
|
||||
отдельно от JWT — тема переживает logout. `PleasePayMeApp` резолвит
|
||||
brightness и прокидывает его в `CupertinoTheme` + `MediaQuery.platformBrightness`.
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-20 — Android nav bar в тёмной теме
|
||||
|
||||
Системная нижняя панель: прозрачный чёрный (`#00000000`), без contrast
|
||||
scrim (`enforceNavigationBarContrast=false`). Иконки светлые. Edge-to-edge
|
||||
включается в `main`, стиль окна — в `values-night`.
|
||||
@@ -0,0 +1,6 @@
|
||||
# 2026-09-20 — Сплэш Please Pay Me
|
||||
|
||||
Фирменный экран запуска: зелёный знак ₽, название, слоган, спиннер.
|
||||
Нативный Android (вкл. 12+ splash API) и iOS launch — тот же фон
|
||||
`#F2F2F7` / `#000000`, чтобы не было вспышки. `main` рисует сплэш сразу
|
||||
и держит его минимум 850 мс, пока восстанавливается сессия.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 2026-09-20 — Правовые документы
|
||||
|
||||
Публичные страницы `/legal/offer|privacy|consent|cookies`.
|
||||
Футер на логине и в кабинете. Cookie-баннер при первом заходе
|
||||
(технические / все, отказ от Метрики). Чекбоксы оферты и согласия
|
||||
на входе в web и mobile; в профиле — «Правовая информация» со
|
||||
ссылками на веб-версии. Реквизиты ИП Архангельский В.А.
|
||||
@@ -0,0 +1,5 @@
|
||||
# 2026-09-20 — Название и иконка
|
||||
|
||||
Отображаемое имя: **Дожить до ЗП**. Иконка: зелёный квадрат #12885A и белый ₽.
|
||||
Проставлена в Android (adaptive), iOS, Windows, favicon кабинета.
|
||||
`AppBrand.name` / `APP_NAME` — единый источник в UI.
|
||||
@@ -0,0 +1,4 @@
|
||||
# 2026-09-20 — Иконка для RuStore
|
||||
|
||||
`mobile/assets/branding/rustore_icon_512.png`: 512×512, 1:1, ~145 КБ.
|
||||
Уменьшена из `app_icon.png` (1024).
|
||||
@@ -0,0 +1,6 @@
|
||||
# 2026-09-20 — Скачать APK с сайта
|
||||
|
||||
Ссылка `/dozhit-do-zp.apk` на логине и в футере.
|
||||
Файл: `web/public/dozhit-do-zp.apk` (gitignore, 53 МБ).
|
||||
Перед деплоем веб скопировать свежий
|
||||
`mobile/build/app/outputs/flutter-apk/app-release.apk`.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 2026-09-20 Telegram → Yandex identity link
|
||||
|
||||
Вход везде только через Яндекс. Бот больше не выдаёт JWT по сырому Telegram `user_id`: пока аккаунт не связан с Яндексом, `POST /api/auth/internal` отвечает 403 `yandex_required` и ссылкой `https://please-pay-me.ru/?tg_link={token}`.
|
||||
|
||||
После входа через Яндекс веб вызывает `POST /api/auth/telegram-link/complete`. API пишет `telegram_yandex_links` и переносит бюджеты / траты / работы с Telegram-id на Yandex-namespaced id (`1<<50 | yandexId`). Дальше бот, кабинет и приложение работают с одним `user_id`.
|
||||
|
||||
`POST /api/auth/telegram` отключён (403). Старые Telegram-сессии в кабинете сбрасываются.
|
||||
Reference in New Issue
Block a user