feat: Docker, health check, schema migrations y webhook opt-in

- migrations/001_initial_schema.sql: DDL idempotente con CREATE TABLE/INDEX IF NOT EXISTS
- database.py: run_migrations() aplica .sql pendientes en orden; init_db() queda como alias
- health.py: servidor aiohttp en puerto 8080 — GET /health, GET /health/history, POST /webhook
  Watcher loop alerta al admin por Telegram si el poller no actualiza en N horas
- config.py: webhook_url, webhook_secret, health_port, health_alert_hours
- poller.py: on_poll_done callback (conecta con health.record_poll)
- main.py: arranca HealthServer, llama run_migrations, modo webhook opt-in via WEBHOOK_URL
- Dockerfile + docker-compose.yml + .dockerignore
- pyproject.toml: aiohttp>=3.9

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
markosbenitez
2026-05-25 23:31:18 -03:00
parent 537ed6931d
commit 076bc100af
11 changed files with 886 additions and 68 deletions

58
main.py
View File

@@ -1,8 +1,8 @@
"""
main.py — Entrypoint de Pedrito Hechakuaa.
Arranca el bot de Telegram y el poller en paralelo.
Al arrancar: inicializa la DB y migra el tenant principal del .env si no existe.
Arranca el bot de Telegram, el poller y el servidor HTTP de health check en paralelo.
Al arrancar: aplica migraciones y migra el tenant principal del .env si no existe.
"""
from __future__ import annotations
@@ -13,9 +13,11 @@ import sys
import structlog
from telegram.ext import Application
import health as health_mod
import vault as vault_mod
from config import get_settings
from database import get_tenant, init_db, save_tenant
from database import get_tenant, run_migrations, save_tenant
from health import HealthServer, record_poll
from onboarding import OnboardingFlow
from poller import Poller
from telegram_bot import PedritoBot
@@ -77,10 +79,11 @@ async def main() -> None:
poll_interval_minutes=settings.poll_interval_minutes,
horario=f"{settings.poll_hora_inicio}:00-{settings.poll_hora_fin}:00",
chat_id=settings.telegram_chat_id,
webhook_mode=bool(settings.webhook_url),
)
# Base de datos
await init_db(settings.db_path)
# Migraciones y tenant principal
await run_migrations(settings.db_path)
await _migrar_tenant_principal(settings)
# Bot de Telegram
@@ -92,19 +95,20 @@ async def main() -> None:
)
app: Application = pedrito_bot.build()
# Poller (sin snapshot global — maneja uno por tenant internamente)
# Poller
poller = Poller(
settings=settings,
bot=pedrito_bot,
on_poll_done=record_poll,
)
# El callback de /notif pasa el chat_id al poller
pedrito_bot.set_notif_callback(
lambda chat_id: poller.revisar_ahora(chat_id=chat_id, es_manual=True)
)
# Manejo de señales para shutdown limpio
loop = asyncio.get_running_loop()
health_server: HealthServer | None = None
def handle_shutdown(sig: int) -> None:
logger.info("shutdown_recibido", signal=sig)
@@ -117,26 +121,52 @@ async def main() -> None:
except NotImplementedError:
pass # Windows no soporta add_signal_handler
# Arrancar bot y poller en paralelo
async with app:
await app.initialize()
await app.start()
logger.info("bot_telegram_iniciado")
telegram_task = asyncio.create_task(
app.updater.start_polling(drop_pending_updates=True)
)
# Servidor HTTP de health check (y webhook si está configurado)
health_server = HealthServer(settings=settings, application=app)
await health_server.start()
if settings.webhook_url:
# Registrar el webhook con Telegram
webhook_url = f"{settings.webhook_url.rstrip('/')}/webhook"
await app.bot.set_webhook(
url=webhook_url,
secret_token=settings.webhook_secret or None,
drop_pending_updates=True,
)
logger.info("webhook_registrado", url=webhook_url)
telegram_task = asyncio.create_task(asyncio.sleep(0)) # placeholder
else:
# Long-polling
telegram_task = asyncio.create_task(
app.updater.start_polling(drop_pending_updates=True)
)
poller_task = asyncio.create_task(poller.run_forever())
logger.info("pedrito_hechakuaa_corriendo")
try:
await asyncio.gather(telegram_task, poller_task)
if settings.webhook_url:
await poller_task
else:
await asyncio.gather(telegram_task, poller_task)
except asyncio.CancelledError:
pass
finally:
await app.updater.stop()
if health_server:
await health_server.stop()
if settings.webhook_url:
try:
await app.bot.delete_webhook()
except Exception:
pass
else:
await app.updater.stop()
await app.stop()
logger.info("pedrito_hechakuaa_detenido")