165 lines
6.5 KiB
Markdown
165 lines
6.5 KiB
Markdown
# Baton — Экстренный сигнал
|
||
|
||
PWA-приложение для отправки экстренных сигналов с геолокацией через Telegram-бота.
|
||
|
||
## Стек
|
||
|
||
- **Backend:** Python 3.12+, FastAPI, aiosqlite, httpx
|
||
- **Frontend:** Vanilla JS PWA (Service Worker, Web Push)
|
||
- **База данных:** SQLite (WAL mode)
|
||
- **Уведомления:** Telegram Bot API
|
||
|
||
## Запуск
|
||
|
||
```bash
|
||
# Зависимости
|
||
pip install -r requirements.txt
|
||
|
||
# Переменные окружения (см. .env.example)
|
||
cp .env.example .env
|
||
|
||
# Запуск
|
||
uvicorn backend.main:app --host 0.0.0.0 --port 8000
|
||
```
|
||
|
||
## Переменные окружения
|
||
|
||
| Переменная | Обязательна | Описание |
|
||
|---|---|---|
|
||
| `BOT_TOKEN` | да | Токен Telegram-бота |
|
||
| `CHAT_ID` | да | ID чата для уведомлений |
|
||
| `WEBHOOK_SECRET` | да | Секрет для верификации Telegram webhook |
|
||
| `WEBHOOK_URL` | да | Публичный URL `/api/webhook/telegram` |
|
||
| `DB_PATH` | нет | Путь к SQLite-файлу (по умолчанию `baton.db`) |
|
||
| `FRONTEND_ORIGIN` | нет | Разрешённый origin для CORS (по умолчанию `http://localhost:3000`) |
|
||
| `WEBHOOK_ENABLED` | нет | Регистрировать webhook при старте (по умолчанию `true`) |
|
||
| `APP_URL` | нет | Публичный URL приложения для keep-alive (например `https://baton.fly.dev`) |
|
||
|
||
## API
|
||
|
||
| Метод | Путь | Описание |
|
||
|---|---|---|
|
||
| `GET` | `/health` | Health check: `{"status": "ok", "timestamp": <unix_ts>}` |
|
||
| `POST` | `/api/register` | Регистрация пользователя |
|
||
| `POST` | `/api/signal` | Отправка экстренного сигнала |
|
||
| `POST` | `/api/webhook/telegram` | Telegram webhook |
|
||
|
||
## Hosting & Keep-Alive
|
||
|
||
### Проблема cold start
|
||
|
||
На бесплатных хостингах (Render, fly.io free tier, Railway и подобных) приложение **засыпает** после периода неактивности (обычно 15–30 минут). Следующий входящий запрос ждёт пока процесс поднимется заново — **cold start занимает 3–5 секунд**. Для экстренного приложения это критично.
|
||
|
||
### Решения по вариантам хостинга
|
||
|
||
| Вариант | Стоимость | Cold start | Рекомендация |
|
||
|---|---|---|---|
|
||
| **fly.io Hobby** | $5/мес | Нет (всегда активен) | Оптимально для прода |
|
||
| **fly.io free tier** | Бесплатно | 3–5 сек | Только для разработки |
|
||
| **Render free** | Бесплатно | 3–5 сек | Только для разработки |
|
||
| **Самохостинг (VPS)** | От $3–5/мес | Нет | Полный контроль |
|
||
|
||
> **Финальный выбор хостинга зависит от решения по OQ-004** (открытый вопрос по бюджету и масштабированию проекта).
|
||
|
||
### Keep-alive механизм (asyncio background task)
|
||
|
||
Приложение запускает фоновый asyncio-таск, который каждые **10 минут** пингует собственный `/health` endpoint. Это предотвращает засыпание на платформах, которые реагируют на активность процесса.
|
||
|
||
**Активация:** установите переменную `APP_URL`:
|
||
|
||
```bash
|
||
APP_URL=https://your-app.fly.dev
|
||
```
|
||
|
||
Без `APP_URL` таск не запускается (keep-alive отключён).
|
||
|
||
**Ограничение:** self-ping работает пока процесс жив. Если платформа убивает процесс при нулевом трафике — нужен внешний пингер (см. ниже).
|
||
|
||
### Keep-alive для самохостинга (cron / systemd timer)
|
||
|
||
Если приложение на VPS и нужен мониторинг извне:
|
||
|
||
**Вариант 1 — crontab:**
|
||
|
||
```bash
|
||
# Редактируем cron
|
||
crontab -e
|
||
|
||
# Добавляем запись (каждые 10 минут):
|
||
*/10 * * * * curl -sf https://your-app.example.com/health > /dev/null
|
||
```
|
||
|
||
**Вариант 2 — systemd timer:**
|
||
|
||
Создайте два файла:
|
||
|
||
`/etc/systemd/system/baton-keepalive.service`:
|
||
```ini
|
||
[Unit]
|
||
Description=Baton keep-alive ping
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=curl -sf https://your-app.example.com/health
|
||
```
|
||
|
||
`/etc/systemd/system/baton-keepalive.timer`:
|
||
```ini
|
||
[Unit]
|
||
Description=Run Baton keep-alive every 10 minutes
|
||
|
||
[Timer]
|
||
OnBootSec=1min
|
||
OnUnitActiveSec=10min
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
```
|
||
|
||
Активация:
|
||
```bash
|
||
systemctl daemon-reload
|
||
systemctl enable --now baton-keepalive.timer
|
||
systemctl list-timers baton-keepalive.timer
|
||
```
|
||
|
||
## Nginx deployment
|
||
|
||
Для проксирования через nginx используйте готовый шаблон `nginx/baton.conf`.
|
||
|
||
### Применение
|
||
|
||
```bash
|
||
# 1. Скопировать шаблон и заменить домен
|
||
sudo cp nginx/baton.conf /etc/nginx/sites-available/baton
|
||
sudo sed -i 's/<YOUR_DOMAIN>/baton.example.com/g' /etc/nginx/sites-available/baton
|
||
|
||
# 2. Получить TLS-сертификат (если ещё нет)
|
||
sudo certbot certonly --nginx -d baton.example.com
|
||
|
||
# 3. Включить конфиг
|
||
sudo ln -s /etc/nginx/sites-available/baton /etc/nginx/sites-enabled/baton
|
||
|
||
# 4. Проверить и применить
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
```
|
||
|
||
### Защита BOT_TOKEN в логах
|
||
|
||
Конфиг включает `map`-блок, который автоматически маскирует токен бота в `access_log`:
|
||
|
||
```
|
||
# В логе вместо реального токена:
|
||
GET /bot<TOKEN>/sendMessage → GET /bot[REDACTED]/sendMessage
|
||
```
|
||
|
||
Это защита по принципу «defence in depth»: текущий webhook-эндпоинт (`/api/webhook/telegram`) токен в URL не содержит, но маскировка сработает, если в будущем появится маршрут вида `/bot<TOKEN>/...`.
|
||
|
||
Заголовок `X-Telegram-Bot-Api-Secret-Token` не попадает в `access_log` — nginx не логирует заголовки запросов в стандартном `log_format`.
|
||
|
||
## Тесты
|
||
|
||
```bash
|
||
pip install -r requirements-dev.txt
|
||
pytest
|
||
```
|