docs/frontend.es.md
Rama main · réplica de lectura del repositorio original.
---
source_sha: "8ca784f8a0b0"
---
# El código de la consola { #the-consoles-code }
[Arquitectura](architecture.md) es el backend. Esta es la otra mitad: la
aplicación Next.js en `frontend/`, para quien está a punto de tocarla.
**Stack.** Next.js 15 (App Router) · React 19 · TypeScript strict · Tailwind ·
`next-intl` · TanStack Query · Zustand · vitest y Testing Library ·
Playwright. Gestor de paquetes y runner: **bun**.
## Dónde vive cada cosa { #where-things-live }
| Ruta | |
| -------------------------------- | -------------------------------------------------------------------------------------- |
| `src/app/[locale]/(dashboard)/…` | El producto. Las rutas llevan **prefijo de locale** |
| `src/app/api/…` | Route handlers que hacen de proxy hacia el backend |
| `src/lib/` | Clientes de API tipados, `query-keys.ts` y los registries de más abajo |
| `src/hooks/` | Uno por recurso — `use-agents`, `use-permissions`, … |
| `src/stores/` | Zustand, uno por asunto |
| `src/components/<domain>/` | La UI por dominio; los primitivos en `ui/`, los estados vacíos y de error en `states/` |
Los Server Components son lo predeterminado. `"use client"` es para state,
effects y handlers, no por costumbre.
## Presentación de la consola { #console-presentation }
Usa `PageHeader` para títulos y acciones compactos. Los catálogos de tarjetas
usan `ListCard layout="grid"` sin un panel adicional; las tablas conservan el
panel. Los botones usan tokens neutros `action`, independientes de `primary`
para controles seleccionados e insignias. El foco y los bordes de los campos
siguen siendo visibles.
Las personas y organizaciones usan `EntityAvatar`: las fotos se recortan en
círculos, con iniciales sobre un color estable como alternativa. Los agentes
conservan sus caras generadas. Las descripciones del catálogo reservan dos
líneas; al abrir un elemento se muestra la descripción completa.
Las acciones asíncronas usan `Button loading` para conservar su etiqueta y tamaño y evitar envíos repetidos. El estado continúa hasta completar todos los pasos, incluidas las cargas de archivos. Los botones basados en enlaces conservan un único hijo y bloquean la activación mientras no están disponibles. Los controles comparten el foco de teclado y tamaños compactos en escritorio, con objetivos táctiles de 44px cuando hay espacio. Las capas se animan desde su anclaje; las ayudas ajustan el texto al ancho de la ventana. El calendario usa el idioma de la consola y el progreso expone su valor real y máximo a las tecnologías de asistencia. Las animaciones respetan la preferencia Motion de la persona.
## El navegador nunca llama al backend { #the-browser-never-calls-the-backend }
Cada petición va a `/api/*` de esta app, que la reenvía a FastAPI con el access
token tomado de una **cookie HttpOnly**. Eso es lo que mantiene el token fuera
del JavaScript y la URL del backend fuera del bundle del cliente.
Lo hace un único reenviador — `src/lib/platform-proxy.ts` — en lugar de un
archivo de ruta hecho a mano por endpoint que repite las mismas doce líneas.
!!! warning "Una respuesta sin `Cache-Control` no es una respuesta que nadie cachee"
El proxy estampa `no-store` en todo lo que el backend dejó sin marcar. Cada
respuesta de aquí depende de una cookie, de un conjunto de permisos y de la
cabecera de organización, y una lista que se vuelve a pedir tras una
escritura tiene que llegar al servidor.
Un archivo de ruta hecho a mano debe la misma cabecera — el proxy es el
único sitio que la aplica por ti.
## Los datos, y dónde vive el state { #data-and-where-state-lives }
**Todo acceso a la API pasa por un cliente de `src/lib/` y se consume a través
de un hook.** Nada de `fetch` en un componente.
Los datos del servidor viven en la capa de queries; **los stores guardan solo
state de UI y efímero**. Registra cada query key en `query-keys.ts` para que la
invalidación tras una escritura se mantenga consistente.
## Historial del chat y actualizaciones del stream { #chat-history-and-streamed-updates }
La consola abre la página más reciente del transcript y carga mensajes anteriores
bajo demanda. Los chips de proyecto consultan `/conversations/{id}/metadata`, con
las mismas comprobaciones de lector y organización, sin cargar mensajes. Las
lecturas del repositorio omiten el resumen almacenado del modelo, salvo que la
reconstrucción de su historial lo solicite explícitamente.
Los sockets del chat entregan ráfagas una vez por frame de animación, con una
alternativa de 50 ms para pestañas ocultas y un límite de cola de 256 frames.
Los deltas contiguos de texto o razonamiento se combinan; los límites de control,
herramientas y partes conservan su orden. Cambiar de conversación, cuenta u
organización descarta los frames pendientes.
Se reutilizan el Markdown y los resultados de herramientas sin cambios. El
desplazamiento sigue el crecimiento medido del contenido. Los dibujos terminados
se montan cerca del viewport y se desmontan fuera, conservando su espacio medido;
sus scripts se reinician al volver. Los dibujos en stream siguen activos y la
pantalla completa suspende la copia del transcript.
## Los permisos son una decisión de renderizado { #permissions-are-a-rendering-decision }
`use-permissions.ts` da el conjunto de permisos efectivo para la organización
activa. **Un control que quien llama no puede usar no se renderiza** — no se
renderiza, en vez de renderizarse y devolver 403.
Dos trampas, y las dos ya han llegado a producción aquí:
- **Qué roles ofrece un selector es aritmética, no una lista.**
`assignableRoles` refleja la regla del servidor sobre el catálogo de permisos:
un rol solo se ofrece cuando el de quien llama lo supera estrictamente. Un
"todos los roles menos owner" cableado a mano es lo que le ofreció a un Admin
la opción Admin y devolvió 403 una vez tecleado el correo.
- **Una página que nombra una organización en su URL _es_ esa organización.** El
cliente de API estampa `X-Organization-Id` desde la organización _activa_, así
que una página que actúa sobre la organización de su ruta mientras lee los
permisos de la activa decide sobre los miembros de Acme según tu rol en
Globex. La adopción vive en `ActiveOrgGuard`, una sola vez.
## Cada string visible para el usuario pasa por `next-intl` { #every-user-facing-string-goes-through-next-intl }
`make lint` lo impone en las dos direcciones: un string legible metido en un
componente falla, y también falla una clave que el catálogo guarda y que ningún
componente lee.
Tres reglas con las que la gente tropieza:
- **Un recuento es un `plural` de ICU, nunca un ternario.**
`{n} file{n === 1 ? "" : "s"}` es una frase que solo el inglés construye así.
- **Un sustantivo con el que concuerda la frase no es un parámetro.**
`{matched} of {total} {noun}` renderiza `3 of 40 skills` en polaco. El
sustantivo va dentro del `plural` o del `select`.
- **El catálogo guarda copy, y solo copy.** Un falso positivo se responde con un
`i18n-exempt` y su motivo; nunca con una clave. Responder a uno moviendo una
lista de clases de Tailwind a `en.json` es la forma en que traducir un string
una vez dejó a un componente sin sus estilos.
El inglés es la lengua de origen y se fusiona bajo cada locale, así que una
traducción que falta renderiza inglés en lugar de la clave.
!!! info "Los nombres propios del producto siguen en inglés en cada locale"
agent, spec, capability, skill, embed, budget, run, prompt, provider, token,
vault, workspace, sandbox, MCP. Nombran cosas que un cliente también
encuentra en la documentación, en la API y en el YAML exportado — traducirlos
en la UI y en ningún otro sitio crea dos vocabularios para un mismo producto.
Flexiónalos, no los sustituyas.
## Cuatro registries, y ninguna segunda fuente { #four-registries-and-no-second-source }
Cada uno de ellos es una tabla que leen varias partes de la UI. Añadir a la
tabla es el cambio entero; añadir una segunda fuente es el bug.
| | Guarda | Se añade con |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `lib/tool-catalog.ts` | El icono, el rótulo mientras corre, el nombre al terminar y el renderizador de cada tool que registra el backend | Una fila indexada por el id de tool de la capability. Un test de backend compara las dos en ambas direcciones |
| `lib/brand-glyphs.generated.ts` | Cada marca de servicio, de connector y de provider, como datos de path en bruto | Una fila en `scripts/gen-brand-icons.ts`, y después `bun run gen:brand-icons`. Nunca un import de un paquete de iconos |
| `lib/dashboard/registry.ts` | Los widgets del dashboard y el permiso al que está sujeto cada uno | Cinco ediciones, enumeradas más abajo en la página |
| `lib/dialog-sizes.ts` | Un token de anchura y un token de forma por diálogo | Eligiendo un token, nunca una altura a medida |
## Dos cosas que debe una superficie nueva { #two-things-a-new-surface-owes }
Las dos son registries con el mismo modo de fallo: una página añadida en
cualquier otro sitio simplemente no está, nada falla, y la funcionalidad se
publica invisible.
**Una parada del recorrido.** `lib/onboarding/tour.ts` es el recorrido pasivo
que reproduce el "?" de una página, y `flows.ts` es la creación guiada que
ofrece al final. Una página sin parada no renderiza **ningún "?"** — así que una
página nueva cuya cabecera no tiene botón de ayuda no se ha registrado. Sujeta
el paso al permiso que lleva su control, márcalo `optional` cuando el control
necesita que existan datos, y áncralo en algo acotado.
**Un widget del dashboard**, si la funcionalidad produce un estado que alguien
querría ver de un vistazo. Cinco ediciones: el id y la definición en
`dashboard/registry.ts`, el componente en `components/dashboard/widgets/`, una
colocación en `layouts.ts`, el id replicado en
`backend/app/schemas/dashboard_layout.py` — un test falla cuando esos dos se
desvían — y el copy en `en.json` y en cada traducción que hay a su lado.
## Verificar { #verify }
Desde `frontend/`. En la raíz del repositorio vitest no encuentra configuración,
informa de unos 164 fallos fantasma y deja un directorio de caché perdido.
```bash
bunx vitest run src/components/chat/usage-meter.test.tsx # while writing
```
Una vez, antes de hacer push — desde la raíz del repositorio:
```bash
make lint-frontend && make test-frontend-cov && make build-frontend
```
!!! danger "`test:coverage`, no `test:run`"
El job que ejecuta CI mide la cobertura y falla por debajo del 100 % de
líneas, sentencias y funciones, o del 97,5 % de ramas. Una suite en la que
pasan todos los tests puede estar roja igualmente, y lo ha estado.
Borrar una rama muerta es más fácil que cubrirla: un `?? ""` detrás de una
comprobación que ya demostró el valor es de los que el gate hace bien en
señalar.
**Un spec que agota su tiempo suele ser la máquina.** `testTimeout` es 15 s y
`asyncUtilTimeout` 5 s, los dos medidos y no adivinados. Ninguno de los dos es
razón para conservar un spec que monta más de lo que leen sus assertions.
## Recapitulación { #recap }
- El navegador habla con **`/api/*` de esta app**, nunca con FastAPI — un único
reenviador, y estampa `no-store`.
- Los datos del servidor viven en la **capa de queries**; los stores guardan
solo state de UI.
- Un control que quien llama no puede usar **no se renderiza**, y los selectores
de rol son **calculados, no listados**.
- El copy pasa por **`next-intl`**, los recuentos son plurales de ICU, y el
catálogo guarda copy y solo copy.
- Una superficie nueva debe una **parada del recorrido**, y un widget si tiene
estado legible de un vistazo — las dos son registries silenciosos.
[El backend →](architecture.md) · [Añadir una funcionalidad →](adding_features.md) ·
[Pruebas →](testing.md)
## Pruebas en la consola { #evals-in-the-console }
Las pruebas reutilizan `PageHeader`, `ListCard`, tamaños de diálogo, pestañas y `WizardSteps`. Los cambios actualizan las consultas registradas mediante `/ws/events`; una evaluación humana también actualiza suites, sesiones de mejora y protecciones de entorno. Un error de lectura ofrece reintentar en lugar de una vista vacía de éxito. Las vistas estrechas conservan campos de 16 px y espacio para el Arquitecto.
Los datasets conservan sus permisos y grants y pueden reutilizarse entre agents. El modelo e instrucciones de calificación pertenecen al dataset; generación y mejora tienen Settings. Los ayudantes definidos en código conservan identidades internas para atribución sin aparecer en el catálogo de agents de usuario. Casos y evals usan las tablas compartidas de la consola; los detalles de casos y review se abren en paneles laterales.
El Arquitecto no recibe una nueva capability para escribir datasets en este hito. Crear preguntas es una acción expresamente estimada y autorizada del flujo de mejora; el asistente de la esquina no puede iniciar silenciosamente ese trabajo de pago. Su botón sigue accesible junto a las vistas de pruebas.
`e2e/improvement.mobile.spec.ts` recorre la API y el worker reales. Comprueba casos, editor, evaluación, revisión, resultados, asistente de mejora y publicación confirmada con protección del entorno. Solo el modelo externo se sustituye por una implementación de prueba. GitHub Actions conserva capturas claras y oscuras a anchuras de teléfono y escritorio para la revisión visual.