Portal FastAPI + 5 endpoints REST para Bootcamp Agentic AI con watsonx Orchestrate (FactorIT). Single container, Coolify-ready. - Landing brandeado FIT con formulario de registro (honeypot anti-bot) - Tokens itsdangerous para descargas (24h expiry) - 5 endpoints API: historical/available procedures, member-insights, schedule, generate-report (Jinja2 + Plotly) - SQLite con upsert-on-email para leads + log de descargas - Admin endpoints (HTTP Basic): leads.json, leads.csv, stats - 23 tests pytest pasando - Dockerfile listo para Coolify con volúmenes persistentes (/app/leads.db, /app/app/data/reports_output, /app/material) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 KiB
Diseño — Portal taller-wox.fitlabs.dev
Fecha: 2026-05-12
Deadline workshop: 2026-05-14 (jueves, 48h)
Owner: Felipe Arentsen (FactorIT)
Dominio: https://taller-wox.fitlabs.dev
Hosting: VPS propio gestionado por Coolify
Estado: validado en brainstorming, listo para writing-plans
Este documento consolida las decisiones tomadas durante la sesión de brainstorming del 2026-05-12 sobre el
SPEC_taller_wox_fitlabs.mdoriginal. El SPEC sigue siendo la fuente de detalle para schemas, paletas, código de referencia y datasets de los endpoints. Este doc lo modifica donde corresponda y deja explícito qué queda fuera de alcance.
1. Contexto y restricciones
1.1 Qué es esto
Portal web + API para el "Bootcamp Agentic AI con watsonx Orchestrate" de FactorIT. Dos funciones simultáneas:
- Frontend público + registro de leads + descarga de material del bootcamp.
- 5 endpoints REST consumidos en vivo por los agentes de watsonx Orchestrate que los alumnos construyen durante el taller.
1.2 Restricciones que mandan el diseño
- 48 horas hasta el primer uso real (taller del jueves 2026-05-14).
- Uso recurrente posterior con cohortes futuras → el diseño debe poder evolucionar, no necesita ya soportar la evolución.
- Cohorte pequeña (~10-30 alumnos) → no necesita escalar.
- Hosting Coolify → todo va en un container Docker, HTTPS automático con Let's Encrypt, volúmenes persistentes declarados en la UI.
- Watsonx Orchestrate rechaza HTTPS con certs inválidos → Let's Encrypt automático resuelve esto.
1.3 Camino crítico para el jueves
- Los 5 endpoints
/api/*funcionando y reachables desde watsonx (HTTPS válido). - La página de descargas con los 2 ZIPs (uno técnico, uno funcional) accesibles.
- El formulario de registro funcionando (lead capture).
Lo demás (admin, polish visual extra, etc.) es nice-to-have.
2. Arquitectura
2.1 Decisión: un solo servicio FastAPI
Una sola app FastAPI corriendo en un container Docker. Sirve 3 superficies en el mismo puerto:
- Frontend + registro:
GET /,GET /descargas,POST /register,GET /download/{file} - API del taller: los 5 endpoints
/api/* - Admin básico:
GET /admin/leads.json,GET /admin/leads.csv,GET /admin/stats(con HTTP Basic auth)
Por qué un solo servicio (y no split): para 48h y una cohorte de ~30 alumnos, los costos operacionales de partir esto en dos servicios (otro container, otra config en Coolify, comunicación interna) superan ampliamente los beneficios. Aceptamos el riesgo de que un bug en el landing pueda afectar la API en vivo; lo mitigamos con un Dockerfile simple, un test smoke pre-deploy, y la decisión explícita de no tocar nada el día del taller.
Cuándo reconsiderar: si en cohortes futuras la API empieza a tener cambios frecuentes (nuevos endpoints por workshop, datasets dinámicos), separar API y frontend en dos servicios independientes pasa a ser justificable.
2.2 Stack
- Backend: Python 3.11+ con FastAPI + Uvicorn (single worker — suficiente para esta escala)
- Templates: Jinja2
- Datos tabulares (endpoints 1 y 2): pandas, CSVs cargados en memoria al boot del servicio
- Charts (endpoint 5): Plotly
- Persistencia: SQLite file-based (
leads.db) - Tokens de descarga:
itsdangerous.URLSafeTimedSerializer, expiración 24h - Frontend: HTML + CSS + JS vanilla, sin framework
Sin Redis, sin Postgres, sin Celery, sin frontend SPA.
2.3 Diagrama lógico
Internet
│
▼
┌─────────────────────────────┐
│ Coolify (VPS, Let's Encrypt)│
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ Container Docker │
│ ┌────────────────────────┐ │
│ │ FastAPI + Uvicorn │ │
│ │ ┌──────────────────┐ │ │
│ │ │ / + /descargas + │ │ │
│ │ │ /register + │ │ │
│ │ │ /download/{file} │ │ │
│ │ ├──────────────────┤ │ │
│ │ │ /api/* (los 5) │ │ │
│ │ ├──────────────────┤ │ │
│ │ │ /admin/* │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────┘ │
│ │
│ Volúmenes persistentes: │
│ • /app/leads.db │
│ • /app/app/data/ │
│ reports_output/ │
│ • /app/material/ │
└──────────────────────────────┘
3. Componentes
3.1 Frontend público (landing)
Estructura visual del SPEC §3.2 sin cambios:
- Hero navy con CTA naranja "Acceder al material"
- 3 cards "¿Qué vas a construir?"
- 4 stats "El taller en números" (4h, 6 módulos, 0 líneas de código, 100% hands-on)
- Sección 4: formulario de registro inline
- Footer navy con logos y créditos
Branding: paleta del SPEC §3.1, tipografía Inter desde Google Fonts. Logo LogoFIT.png (a comprimir, queda en static/img/). Logo IBM watsonx Orchestrate: placeholder de texto estilizado ("powered by IBM watsonx Orchestrate") en el header derecho — se reemplaza por logo oficial cuando esté disponible, sin redeploy si lo dejamos como SVG inline editable.
3.2 Formulario de registro (POST /register)
Campos:
| Campo | Validación |
|---|---|
nombre |
text, min 2, max 80, required |
email |
email válido, required |
empresa |
text, min 2, max 100, required |
consentimiento |
checkbox, debe estar marcado |
website |
honeypot — campo invisible vía CSS display:none; si llega lleno, descartar silenciosamente con 200 OK falso |
Comportamiento con email duplicado: si el email ya existe en la tabla leads, no es error — se actualiza last_seen (nueva columna), se incrementa times_registered, y se emite un token nuevo igual. Fricción cero. El email es lead capture, no auth.
Respuesta exitosa: redirección a /descargas?token=<token>.
Token: itsdangerous.URLSafeTimedSerializer(SECRET_KEY).dumps({"email": email, "nombre": nombre}), expira 24h (TOKEN_EXPIRY_HOURS env).
3.3 Página de descargas (GET /descargas?token=...)
Valida token. Si inválido o expirado → redirige a /?error=token-invalido y la home muestra banner "El link expiró, regístrate de nuevo".
Si válido, renderiza:
-
Saludo "¡Hola {nombre}!"
-
2 cards grandes (cambio importante vs SPEC §3.4 que tenía 8-9 cards):
- 🧩 Material técnico →
taller-wox-tecnico.zipDescripción: "Specs OpenAPI, configs y artefactos para importar a watsonx Orchestrate" - 📚 Material funcional →
taller-wox-funcional.zipDescripción: "Manual paso a paso del bootcamp y deck de slides"
Cada card: icono, título, descripción 1 línea, tamaño en MB (calculado dinámicamente al servir la página), botón naranja "Descargar".
- 🧩 Material técnico →
Cards hardcodeadas en el template (template Jinja2 + lista Python en app/frontend.py). Si en el futuro se quiere editar copy sin redeploy, pasar a app/data/downloads.json.
Contenido de los ZIPs: Felipe los arma y los sube vía SFTP / file manager de Coolify directo al volumen /app/material/. El backend no decide, no genera, no transforma — solo sirve lo que esté en esa carpeta.
3.4 Descarga (GET /download/{filename}?token=...)
- Valida token query param. Si inválido → 401.
- Verifica que
filenameesté en una whitelist hardcodeada ({"taller-wox-tecnico.zip", "taller-wox-funcional.zip"}) para prevenir path traversal. - Sirve el archivo desde
/app/material/{filename}conContent-Disposition: attachment. - Registra la descarga en la tabla
downloads(lead_email, filename, ip, downloaded_at) — útil para/admin/stats.
3.5 API del taller (los 5 endpoints)
Todos los endpoints del SPEC §5 se mantienen exactamente como están especificados (mismo contrato, mismo schema, mismas respuestas). Esta sección solo enumera el alcance y enlaza al SPEC para el detalle.
| # | Endpoint | Tipo | Detalle |
|---|---|---|---|
| 1 | POST /api/historical-procedures |
Filtros + group_by sobre CSV histórico | SPEC §5.1 |
| 2 | POST /api/available-procedures |
Filtros + group_by sobre CSV catálogo | SPEC §5.2 |
| 3 | GET /api/member-insights |
JSON estático del afiliado demo | SPEC §5.3 |
| 4 | GET /api/schedule |
Guía estática de scheduling | SPEC §5.4 |
| 5 | POST /api/reports/generate-report |
Genera HTML con Jinja2 + Plotly, guarda en disco, devuelve URL pública | SPEC §5.5 |
CORS: allow_origins=["*"], allow_methods=["GET", "POST"] — necesario para que los agentes de watsonx Orchestrate puedan llamar los endpoints desde *.cloud.ibm.com.
Sin rate limiting, sin cleanup automático de los HTMLs generados (volumen esperado: ~150 archivos × 30 KB = 5 MB por taller, despreciable; si crece, se limpia con un cron manual o rm puntual).
Datasets sintéticos (los genero yo siguiendo el SPEC §9): ver sección 6 de este doc.
3.6 Admin básico
3 endpoints, todos detrás de HTTP Basic auth (ADMIN_USER + ADMIN_PASS desde env):
GET /admin/leads.json— lista paginada de leads (?limit=100&offset=0)GET /admin/leads.csv— exporta todo como CSV descargable (Excel-friendly, UTF-8 con BOM para que Excel respete acentos)GET /admin/stats— JSON con:total_leads,total_downloads,downloads_por_archivo,top_5_empresas
Sin UI, sin gráficos, sin filtros. JSON crudo + CSV download.
4. Datos y persistencia
4.1 Esquema SQLite
CREATE TABLE IF NOT EXISTS leads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
empresa TEXT NOT NULL,
ip TEXT,
user_agent TEXT,
consent INTEGER NOT NULL DEFAULT 0,
times_registered INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS downloads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
lead_email TEXT NOT NULL,
filename TEXT NOT NULL,
ip TEXT,
downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_downloads_email ON downloads(lead_email);
CREATE INDEX IF NOT EXISTS idx_downloads_filename ON downloads(filename);
Cambio vs SPEC §4.1: agregué UNIQUE en email, times_registered, last_seen para soportar la lógica de duplicados decidida en brainstorming.
4.2 Lo que va en disco persistente
| Path en container | Qué contiene | Volumen Coolify |
|---|---|---|
/app/leads.db |
SQLite con leads y downloads |
sí |
/app/app/data/reports_output/ |
HTMLs generados por endpoint 5 | sí |
/app/material/ |
Los 2 ZIPs que sube Felipe | sí |
4.3 Lo que va dentro del container (no persistente, viaja con la imagen)
- Código de la app (
/app/app/) - Datasets sintéticos de los endpoints (
/app/app/data/*.csv,*.json,*.txt) — son inmutables, cambios pasan por commit + redeploy - Static files (
/app/static/) — CSS, JS, imágenes - Templates Jinja2 (
/app/app/templates/)
5. Deploy
5.1 Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
COPY static/ ./static/
# crear directorios runtime (se montan como volúmenes en prod)
RUN mkdir -p app/data/reports_output material
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Sin healthcheck por simplicidad (Coolify pinguea HTTP / por defecto). Sin multi-stage build — la imagen final pesa ~250 MB y no vale la pena optimizar para 48h.
5.2 Configuración Coolify
Volúmenes persistentes a declarar en la UI:
| Mount point | Tipo |
|---|---|
/app/leads.db |
bind file |
/app/app/data/reports_output/ |
bind dir |
/app/material/ |
bind dir |
Variables de entorno:
SECRET_KEY=<random 64-char string>
ADMIN_USER=felipe
ADMIN_PASS=<random largo>
TOKEN_EXPIRY_HOURS=24
BASE_URL=https://taller-wox.fitlabs.dev
DNS: apuntar taller-wox.fitlabs.dev al IP del VPS de Coolify desde el panel de DNS del registrar. Coolify gestiona el cert Let's Encrypt automáticamente.
5.3 Workflow de actualización
- Cambios de código (endpoints, frontend, templates): commit + push → Coolify rebuildea + redeploy.
- Cambios de los ZIPs descargables: Felipe sube directo al volumen
/app/material/vía SFTP o file manager de Coolify — sin redeploy. - Cambios de copy de las cards de descarga: requieren redeploy (están hardcodeadas). Aceptable por ahora.
6. Datasets sintéticos que tengo que generar
Todos siguiendo los esquemas del SPEC §5 y §9. Coherencia entre datasets es crítica.
6.1 app/data/historical_procedures.csv (~50 filas)
Columnas exactas del SPEC §5.1. Reglas de coherencia:
- Al menos 4 filas con
member_name = Charlie Smithyrelationship = Self, con procedimientos ydateconsistentes con loslast_datedemember_insights.json:Annual Physical Examcondate = 2022-06-15Dental Cleaningcondate = 2023-01-10Vision Examcondate = 2021-08-22Blood Test - Cholesterol Panelcondate = 2023-03-04
- Resto de filas: otros member_names (Alice, Bob, Diana, Ethan) con relationships variados (Self, Spouse, Mother, Father, Son, Daughter), con procedimientos variados que cubren
preventive,surgery,diagnostic. - Costos realistas: X Ray $80-300, MRI $1500-3000, Annual Physical $200-400, Appendectomy $10k-30k, Dental Cleaning $80-200.
in_networkmezcla de true/false (~70/30).member_planmezcla de los 3 planes del SPEC.
6.2 app/data/available_procedures.csv (~25-30 filas)
Columnas del SPEC §5.2. Reglas:
- Cubrir al menos estos
procedure:X Ray,MRI,CT Scan,Annual Physical Exam,Appendectomy,Dental Cleaning,Vision Exam,Blood Test,Angioplasty,Ultrasound. - Cada procedimiento aparece en 2-4 ubicaciones distintas (City Hospital, Regional Medical Center, Green Valley Clinic, Sunrise Health) con costos y distancias distintas.
gold_ppo_plan_acceptedtrue en ~80% de las filas (es el plan de Charlie Smith).- Costos realistas y diferentes a los de
historical_procedures.csv(uno es histórico, otro es catálogo).
6.3 app/data/member_insights.json
JSON estático basado en el SPEC §5.3, con un ajuste obligatorio: las fechas de overdue_procedures del SPEC están escritas con base ~2024 (ej. last_date: "2022-06-15" con due_since_months: 22). Hoy es 2026-05-12, así que esos cálculos están desfasados ~2 años. Hay que re-calibrar last_date y due_since_months para que sean coherentes con la fecha actual y el recommended_frequency_months de cada item. Las fechas resultantes también deben calzar con las filas correspondientes en historical_procedures.csv (sección 6.1).
Ejemplo de re-calibración para Annual Physical Exam (frecuencia recomendada: 12 meses, prioridad alta):
last_date: ~24 meses atrás →2024-05-15due_since_months: 12 (debido hace 1 año)
6.4 app/data/schedule_response.txt
Texto estático tal cual el SPEC §5.4.
6.5 Archivos para endpoint 5 (reportes)
app/data/combined_email.txt— email mezclado proveedor + paciente (ejemplo en SPEC §5.5)app/data/provider_email.txt— email solo del proveedor con jerga médicaapp/data/aetna_email.txt— email de aseguradora con EOBapp/data/aetna_claim_review_summary.csv(~10 filas) — CPT codes con charged/allowed/plan_paid/patient_responsibility coherentes (el último iguala los anteriores menos los descuentos)
7. Estructura de carpetas final
taller-wox/
├── app/
│ ├── main.py # FastAPI app, mounts, CORS, lifespan
│ ├── frontend.py # GET /, GET /descargas, POST /register, GET /download
│ ├── benefits_api.py # endpoints 1-4
│ ├── reports_api.py # endpoint 5
│ ├── admin.py # endpoints /admin/*
│ ├── db.py # SQLite, init schema, helpers
│ ├── security.py # token sign/verify, basic auth, honeypot
│ ├── data/
│ │ ├── historical_procedures.csv
│ │ ├── available_procedures.csv
│ │ ├── member_insights.json
│ │ ├── schedule_response.txt
│ │ ├── combined_email.txt
│ │ ├── provider_email.txt
│ │ ├── aetna_email.txt
│ │ ├── aetna_claim_review_summary.csv
│ │ └── reports_output/ # generados por endpoint 5 (volumen persistente)
│ └── templates/
│ ├── index.html # landing + registro
│ ├── descargas.html # página post-registro
│ └── report.html # wrapper del reporte (endpoint 5)
├── static/
│ ├── css/styles.css
│ ├── img/
│ │ ├── LogoFIT.png # ya existe, comprimir
│ │ └── favicon.ico # generar
│ └── js/app.js
├── material/ # volumen persistente, Felipe sube los ZIPs
│ ├── taller-wox-tecnico.zip
│ └── taller-wox-funcional.zip
├── docs/
│ └── superpowers/
│ └── specs/
│ └── 2026-05-12-taller-wox-design.md # este archivo
├── leads.db # volumen persistente
├── Dockerfile
├── requirements.txt
├── .env.example
└── README.md
8. Cuts explícitos (lo que NO se hace para el jueves)
- ❌ Email transaccional (SMTP)
- ❌ Admin UI / dashboard con gráficos
- ❌ ZIP "todo en uno" como tercera descarga
- ❌ ZIP-por-archivo individual
- ❌ Re-captcha (solo honeypot)
- ❌ Stats avanzados / analytics
- ❌ Multi-idioma (solo español)
- ❌ Editor visual de leads
- ❌ Rate limiting en
/api/* - ❌ Cleanup automático de reportes generados
- ❌ Conversión PPTX → PDF (Felipe arma los ZIPs)
- ❌ Logo IBM watsonx oficial (placeholder de texto hasta tener el oficial)
- ❌ Multi-stage Docker build
- ❌ Healthcheck custom (Coolify usa ping HTTP por defecto)
- ❌ Tests automatizados extensos (solo smoke tests manuales del SPEC §8)
9. Criterios de aceptación para el jueves 2026-05-14
Antes del taller, todo lo siguiente debe estar verde:
Smoke tests manuales:
GET https://taller-wox.fitlabs.dev/devuelve 200 con HTML brandeado FIT- Submit del form con datos válidos redirige a
/descargas?token=... - La página de descargas muestra las 2 cards y el saludo con el nombre
GET /download/taller-wox-tecnico.zip?token=...descarga el ZIP correctoGET /download/taller-wox-funcional.zip?token=...descarga el ZIP correcto- Token inválido → redirige a home con banner de error
- Email duplicado en form → re-emite token sin error
API del taller (con curl desde laptop):
POST /api/historical-procedurescon{"filters":"[]","group_by":"[]"}devuelve filasPOST /api/historical-procedurescon filtromember_name = Charlie Smithdevuelve las 4 filas esperadasPOST /api/available-procedurescon filtroprocedure contains MRIdevuelve resultadosGET /api/member-insightsdevuelve el objeto conoverdue_procedurescon 4 itemsGET /api/scheduledevuelve el texto de schedulingPOST /api/reports/generate-reportcon{"layout_config":"[\"care_report\"]"}devuelve unpublic_urlque abre un HTML válido
End-to-end con watsonx Orchestrate:
- Importar
openapi-tools-spec.jsonen un agente nuevo de watsonx funciona sin errores de TLS - Prompt "¿Estoy atrasado en algún chequeo?" devuelve los 4 procedimientos overdue
- Prompt "¿Cuánto cuesta una resonancia magnética?" devuelve resultados de MRI
Admin:
GET /admin/leads.csvcon basic auth descarga CSV con los leads de pruebaGET /admin/statsdevuelve los conteos esperados
Operacional:
- Coolify rebuildea sin errores al hacer push
- Redeploy NO borra
leads.dbni los ZIPs (volúmenes funcionando) - HTTPS válido (curl al dominio sin warning de cert)
10. Riesgos identificados
| Riesgo | Mitigación |
|---|---|
| Cambio de DNS no propaga a tiempo | Configurar DNS lo antes posible (hoy mismo idealmente), TTL bajo |
| Volúmenes Coolify mal configurados → pérdida de datos en redeploy | Probar un redeploy de prueba antes del miércoles y verificar que leads.db sobrevive |
| Watsonx no puede llamar a la API por problema de TLS o CORS | Smoke test end-to-end con un agente de prueba el miércoles |
| Datasets sintéticos inconsistentes (Charlie Smith con datos que no calzan) | Validar cruzando historical_procedures.csv vs member_insights.json antes de deploy |
| Tamaño del PNG del logo (971 KB) impacta carga del landing | Comprimir a <100 KB con tinypng o similar antes del deploy |
| Bug en código del frontend tumba la API en vivo durante el taller | Política: no tocar código el día del taller. Si hay bug crítico en API, rollback inmediato a la versión anterior en Coolify |
11. Lo que NO está en este diseño y necesita decidirse después del jueves
- Persistencia de los HTMLs generados por endpoint 5 — política de retención (¿borrar después de N días?)
- Si vienen cohortes con datasets distintos, cómo soportar multi-cohort sin redeploy
- Si el admin necesita UI real (dashboard, gráficos)
- Si conviene separar API y frontend en dos servicios
- Si vale la pena agregar webhook a Slack/email cuando se registra un lead nuevo (lead capture activo)
Fin del diseño. Siguiente paso: invocar skill writing-plans para crear el plan de implementación paso a paso.