Imported from ldkjvvier/reporting-system (
AGENTS.md). Install upstream withnpx skills add ldkjvvier/reporting-system. Copyright stays with the author.
AGENTS.md — Contexto del proyecto para modelos LLM
Documento de contexto destinado a un agente LLM (p. ej. Fable 5) que va a leer, razonar o modificar este repositorio. Es denso y factual a propósito. Para humanos ver
README.md(cómo ejecutar) yCONTEXTO.md(propósito y alcance). Si hay conflicto, el código es la fuente de verdad; este archivo orienta dónde mirar.
TL;DR
Sistema de reportería que genera reportes desde Datadog — Cloud SIEM (Logs / Security Signals) y Métricas (timeseries) — los exporta a CSV/Excel y los envía por correo (Microsoft Graph), de forma programada (cron por reporte) o manual. Web (React) + API (FastAPI) + Celery (worker/beat) + PostgreSQL + Redis, orquestado con Docker. Las integraciones externas (Datadog, Azure) son mock-first: el modo se cambia a real por variable de entorno y las credenciales se leen del Vault corporativo (hvac, login userpass), no de env.
Stack y versiones
- Backend: Python (3.12 en Docker, 3.14 en local), FastAPI, SQLAlchemy 2.x, Pydantic v2 + pydantic-settings, Celery 5 + celery-redbeat, pandas + openpyxl, PyJWT, bcrypt.
- Frontend: React 18 + TypeScript, Vite 6, Mantine 7, react-router-dom 6, axios.
- Infra: PostgreSQL 16, Redis 7, Nginx (sirve el frontend en prod), Docker Compose.
Mapa del repositorio (rutas exactas)
backend/
app/
main.py # App FastAPI; monta routers; CORS; GET /api/health
config.py # Settings (pydantic-settings). USE_CELERY, *_MODE, etc.
db.py # engine/SessionLocal/Base; get_db(); session_scope()
models.py # ORM: User, Report, ReportRun
schemas.py # Pydantic I/O + validadores (cron, source_type, format, window)
init_db.py # create_all idempotente (en vez de migraciones)
auth/security.py # hash/verify (bcrypt), JWT, get_current_user
api/
auth.py # POST /api/auth/register, /api/auth/login
reports.py # CRUD /api/reports + POST /{id}/run (manual)
runs.py # GET /api/reports/{id}/runs, GET /api/runs/{id}/download
datadog.py # GET /api/datadog/fields, POST /api/datadog/preview
integrations/
vault.py # Vault corporativo (hvac): get_platform_secret("datadog"|"email")
datadog/{base,mock,real,factory}.py # DatadogClient + impls + selector
email/{base,mock,real,factory}.py # EmailSender + impls + selector
reporting/
builder.py # QueryResult -> DataFrame -> CSV/XLSX en OUTBOX_DIR
service.py # run_report(): orquesta fetch->build->send->registra run
scheduling/
celery_app.py # instancia Celery (broker/back Redis; RedBeat)
tasks.py # run_report_task (Celery)
sync.py # alta/baja de schedule RedBeat por reporte
tests/test_builder.py # pytest: mock determinista + CSV/XLSX
requirements.txt # deps Docker (incluye psycopg2, redis, datadog-api-client)
requirements-local.txt # deps local (SQLite; sin redis/psycopg2/datadog-client)
Dockerfile, entrypoint.sh
.env # SOLO local; gitignored; no va a la imagen
frontend/
src/
main.tsx # bootstrap React + Mantine + Router + AuthProvider
App.tsx # rutas + AppShell + Protected
api.ts # axios; interceptores de token; reportsApi/datadogApi/auth
auth.tsx # AuthContext (login/register/logout, token en localStorage)
types.ts # tipos TS espejo de los schemas
pages/{Login,Dashboard,ReportForm,RunHistory}.tsx
vite.config.ts # proxy /api -> http://localhost:8000 en dev
Dockerfile (multi-stage Nginx), nginx.conf
docker-compose.yml # 6 servicios: db, redis, api, worker, beat, frontend
run-local.ps1 # arranque local sin Docker
.env.example # plantilla de variables (modos mock por defecto)
README.md / CONTEXTO.md / AGENTS.md
Cómo ejecutar
Local (sin Docker, SQLite, ejecución en proceso):
./run-local.ps1 # API :8000, Frontend :5173, usuario demo@empresa.com / demo1234
Config local en backend/.env: USE_CELERY=false, DATABASE_URL=sqlite:///./local.db,
OUTBOX_DIR=./outbox, modos mock. El backend lee .env desde su CWD (backend/).
Docker (stack completo con scheduling real):
cp .env.example .env && docker compose up --build -d
# frontend :8080, api :8000
Tests: cd backend && pytest -q (o make test en Docker).
Flujos clave
Ejecución de un reporte (app/reporting/service.py::run_report):
get_datadog_client().search(source_type, query, time_window)→QueryResult.builder.build_file(...)→ escribe CSV/XLSX enOUTBOX_DIR, devuelve (path, filename, rows).get_email_sender().send(...)→ envía adjunto (mock por defecto).- Crea/actualiza fila en
report_runs(status, row_count, file_path, delivery_status, error).
"Ejecutar ahora" (api/reports.py::run_now): si USE_CELERY → run_report_task.delay();
si no → BackgroundTasks ejecuta run_report en proceso con session_scope().
Programado: al crear/editar/borrar un reporte, scheduling/sync.py registra/elimina una
entrada RedBeat (cron→celery.schedules.crontab). Beat la dispara → run_report_task.
Solo activo con USE_CELERY=true (Docker). En local el cron NO se dispara.
Selección mock/real (patrón factory)
integrations/datadog/factory.py::get_datadog_client()→RealDatadogClientsiDATADOG_MODE=realy el Vault está configurado (vault_is_configured()); si no,MockDatadogClient. Credenciales desde el secretodatadogdel Vault (api_key,app_key, opcionalsite).source_type∈ {signals,logs,metrics}. Campos por fuente endatadog/base.py(SIGNAL_FIELDS/LOG_FIELDS/METRIC_FIELDS);metricsaplana cada punto de la serie a una fila (timestamp, metric, scope, value, unit). Real: Metrics API v1query_metrics.
integrations/email/factory.py::get_email_sender()→GraphEmailSendersiEMAIL_MODE=realy el Vault está configurado; si no,MockEmailSender. Credenciales desde el secretoemaildel Vault (app_id,secret,tenant,username= buzón remitente,pass). Conpassusa el flujo delegado username/password (ROPC); sinpass, client_credentials.- Vault corporativo (
integrations/vault.py): login userpass (hvac) con mount pointVAULT_MOUNT_POINT(defaultuserpass-cybd); secretos en{VAULT_BASE_PATH}/{VAULT_ENVIRONMENT}/{plataforma}(defaultsecurity-management/soar/{qa|silver_swan}/{datadog|email}).get_platform_secret()se cachea por proceso (lru_cache): rotar un secreto ⇒ reiniciar api/worker. Soporta KV v1 y v2. - Las impls
real.pyyvault.pyimportan sus SDKs de forma perezosa (dentro de métodos), así que no se requierendatadog-api-client/httpx/hvacen local. No muevas esos imports al top-level.
Variables de entorno relevantes (config.py)
SECRET_KEY, ACCESS_TOKEN_EXPIRE_MINUTES, CORS_ORIGINS (string CSV; usar
settings.cors_origins_list), DATABASE_URL, REDIS_URL, USE_CELERY, OUTBOX_DIR,
DATADOG_MODE/DATADOG_SITE, EMAIL_MODE,
VAULT_ADDR/VAULT_USERNAME/VAULT_PASSWORD/VAULT_MOUNT_POINT/VAULT_ENVIRONMENT/
VAULT_BASE_PATH. Las credenciales de plataformas (Datadog, Graph) no son env: viven
en el Vault.
Convenciones
- Comentarios y textos de UI en español; identificadores de código en inglés.
- Endpoints bajo prefijo
/api. Auth porBearerJWT (get_current_user). - Cada
Reportpertenece a unUser(owner_id); los endpoints filtran por propietario. - Tipos TS en
frontend/src/types.tsdeben mantenerse en sincronía conschemas.py. columns/recipientsse guardan como JSON; cron en formatom h dom mon dow.
Gotchas / decisiones no obvias (no romper)
CORS_ORIGINSesstr, nolist: pydantic-settings v2 intenta parsear listas como JSON desde.envy falla. Se divide en la propiedadcors_origins_list.- SQLite requiere
connect_args={"check_same_thread": False}(ya endb.py) por las BackgroundTasks en otro hilo. - Mock Datadog determinista: la ventana se ancla al minuto y los datos derivan de un seed
sha256(source_type|query|time_window). Misma config ⇒ mismos datos (la preview coincide con el reporte). El test compara filas ignorando el timestamp exacto. - Esquema BD: se crea con
app/init_db.py(Base.metadata.create_all). No hay Alembic. Si cambiasmodels.py, en local borrabackend/local.dbo ajusta el esquema a mano. - bcrypt trunca la contraseña a 72 bytes (límite del algoritmo) — ver
auth/security.py. - Celery import perezoso:
run_report_taskse importa dentro de la ramaUSE_CELERYenreports.pypara que el modo local no necesite broker.sync.pyimportaredbeatdentro de funciones. Mantener esa pereza. - Build frontend:
tsc && vite build.vite.config.tsqueda fuera deincludedetsconfig.jsona propósito (usaprocessen contexto Node).
Estado actual
- Funcional de extremo a extremo con mocks; verificado: register/login/preview/create/run (success, ~40 filas)/download (XLSX). Corriendo en modo local (SQLite).
- Pendiente: acceso real al Vault corporativo (VAULT_*) con los secretos
datadogyemailcargados, para activar*_MODE=real. - En local el cron automático no dispara (necesita Docker con worker+beat).
- Timezone del scheduler: los cron se interpretan en
settings.SCHEDULER_TIMEZONE(por defectoAmerica/Santiago, horario de Chile).celery_app.timezoneusa ese valor yreports._next_runcalcula conZoneInfo(SCHEDULER_TIMEZONE). La zona es global, no por reporte (el campoReport.timezonees informativo); tz por-reporte = trabajo futuro.
Si vas a extender el proyecto
- Nuevo endpoint: crea router en
app/api/, móntalo enmain.py, agrega schema enschemas.pyy, si toca, tipo enfrontend/src/types.ts+ método enfrontend/src/api.ts. - Nueva fuente/canal: añade una impl a la interfaz correspondiente en
integrations/y amplía el factory; mantén imports de SDK perezosos. - Disparo automático en local: opción sugerida no implementada = APScheduler en proceso
cuando
USE_CELERY=false(ver §"Evoluciones" enCONTEXTO.md). - Cambios de modelo ⇒ recordar que no hay migraciones (Alembic es trabajo futuro).