--- name: web-layer-builder description: Construye la capa web (front + back ligero) que acompaña a la solución WxO. Soporta dos happy-paths: FastAPI+HTMX (estilo Cotemar — demos rápidas, control plane SSR) y FastAPI+React+Vite (estilo Dun — apps productivas). Incluye widget embed WxO, vista de trazas observable, polling pattern. --- # Web Layer Builder Tu trabajo es **construir la capa web** de la solución: la app/landing/panel que el usuario humano va a usar para ver lo que el agente está haciendo. ## Dos modos ### Modo A — FastAPI + HTMX + Jinja (default) **Cuándo:** demos, control planes, kanbans, paneles operativos, dashboards. **Ventajas:** SSR (no build), polling con `hx-trigger` trivial, fácil de mantener por una persona. **Estructura:** ``` web/ ├── Dockerfile ├── app/ │ ├── main.py │ ├── routers/ │ │ ├── pages.py ← rutas que devuelven HTML │ │ ├── api.py ← endpoints JSON (traces, etc.) │ │ └── partials.py ← fragmentos HTMX │ ├── templates/ │ │ ├── base.html ← layout │ │ ├── index.html ← landing con embed WxO │ │ ├── traces.html ← timeline observable │ │ └── _*.html ← partials HTMX │ ├── static/ │ │ ├── css/ │ │ └── js/ │ └── db.py ← SQLite para audit_logs / traces └── requirements.txt ``` ### Modo B — FastAPI + React + Vite + Tailwind **Cuándo:** apps productivas con varios usuarios, mucha interacción, UI rica. **Ventajas:** ecosistema React, state management decente, lazy loading. **Estructura:** ``` web/ ├── backend/ ← FastAPI (puede coincidir con backend-tool-builder) └── frontend/ ├── Dockerfile ← multi-stage: build + nginx ├── package.json ├── vite.config.ts ├── tailwind.config.js ├── nginx.conf ← reverse-proxy /api → backend └── src/ ├── main.tsx ├── App.tsx ├── pages/ ├── components/ ├── hooks/ │ ├── usePolling.ts ← pattern correcto │ └── useDownload.ts ← downloadAuthenticated ├── stores/ ← zustand ├── lib/ │ ├── api.ts │ └── auth.ts └── styles/ ``` ## Patrones obligatorios (ambos modos) ### 1. Embed del chat WxO En la página principal: ```html
``` Recordatorio: WxO Console → Embed Security → **Off** (issue I-007). ### 2. Vista de trazas observable - `GET /traces` página timeline filterable (filtros: agent, tool, fecha) - `GET /api/traces/recent?since=ISO` JSON endpoint - Polling cada 2s para nuevas trazas - Tabla SQLite con índices `(started_at, agent_caller, tool)` - `POST /api/traces` recibe trazas del decorator `@observable_tool` ### 3. Polling correcto (issue I-016) **Modo A (HTMX):** ```html
``` **Modo B (React):** ```ts function usePolling(fetcher: () => Promise, isDone: (data: T) => boolean) { const [data, setData] = useState(null); useEffect(() => { let timer: number; const poll = async () => { const next = await fetcher(); setData(next); if (!isDone(next)) { timer = window.setTimeout(poll, 1500); } }; poll(); return () => clearTimeout(timer); }, []); return data; } ``` ### 4. Healthcheck endpoint `GET /health` → 200 OK. ### 5. Dockerfile con healthcheck correcto ```dockerfile HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget -qO- http://localhost:8000/health || exit 1 ``` ### 6. Si Modo B — downloadAuthenticated (issue I-015) ```ts async function downloadAuthenticated(fetchUrl: string, filename: string) { const res = await fetch(fetchUrl, { headers: authHeaders() }); const blob = await res.blob(); const blobUrl = URL.createObjectURL(blob); // OJO: variable separada const a = document.createElement("a"); a.href = blobUrl; a.download = filename; a.click(); URL.revokeObjectURL(blobUrl); } ``` ### 7. Si Modo B — admin seed + rotation - `SEED_ADMIN_PASSWORD` solo al primer boot - Endpoint `POST /auth/password` para rotar desde UI - Issue I-014 / commit dun b3de2d9 ## Entrada esperada ``` mode: A | B landing: { has_chat_embed: true, kanban_columns: [...], control_plane: true } trace_view: { enabled: true, default_filter: all } custom_pages: [ { path, title, content_description } ] ``` ## Validación - [ ] `/health` responde 200 - [ ] Embed widget carga (con Embed Security OFF) - [ ] `/traces` muestra trazas si las hay - [ ] Polling funciona y no muere después de un click - [ ] Healthcheck del Dockerfile pasa - [ ] Pasa `python3 evals/lint_wxo_yaml.py` (chequea compose)