# Decisiones del owner durante la implementación

Lo que se definió **después** de la investigación y que los agentes de las fases siguientes tienen
que cumplir. Manda sobre los informes por vista: si un informe propone otra cosa, gana esto.

**Cómo se escribe una entrada nueva.** El owner mira la pantalla, marca algo, se corrige, y acá queda
la **regla** — no el diagnóstico. Cuatro partes y nada más: el título en una línea, el comentario
textual, la regla en dos o tres líneas, y a qué fase o vista obliga. Si hay una trampa concreta que
un agente pisaría, va una línea más.

Lo que **no** va: cómo se llegó a la conclusión, qué se probó y se descartó, ni el relato de la
verificación. De D1 a D8 están escritas largo porque salieron de la investigación inicial; de acá en
adelante, cortas.

---

## D1 · La primera pantalla es corta en **todas** las vistas, no sólo en el tablero

> «No me molesta navegar. Lo que me molesta es navegar hacia otra página donde también está todo
> vomitado en un primer plano, cero minimalista.»

Ésta es la regla que gobierna las fases 2 a 4. Un tablero minimalista que al tocar «Ver» deposita a
la persona en una pared de contenido **no arregla nada**: mueve el problema una pantalla más adelante
y encima agrega un clic.

**La regla, verificable al llegar a cualquier vista, sin scrollear:**

1. **Lo que entra en la primera pantalla contesta la pregunta de esa vista, y nada más.** Todo lo
   demás está plegado, en un panel, o a un clic.
2. **Hay una sola acción primaria visible**, o ninguna. Si hay dos botones que compiten, uno está de
   más.
3. **Ningún bloque secundario ocupa lugar cuando no aplica.** Un panel de exportación sin
   exportaciones, una tarjeta de lote sin lote en curso y un filtro sobre una lista de una fila miden
   **cero píxeles**, no «poco».
4. **Ningún dato se escribe dos veces en la misma pantalla.** Si vive en una tarjeta, no vive además
   en prosa; la prosa se queda con lo que la cifra no dice.

**Cómo se comprueba cada tipo de vista** —esto es lo que el orquestador mide con el navegador al
cerrar cada fase, en 1440×900—:

| Tipo de vista | Qué tiene que verse al llegar, sin scroll |
|---|---|
| Tablero | Los aspectos, completos. Nada más |
| Lista (dominios, cobertura, lotes, sitemaps, sesiones, avisos, claves) | El recorte vigente, el total, y **al menos las primeras cinco filas** |
| Ficha (dominio, lote) | Identidad, estado con su fecha, la acción principal y las cifras que la explican |
| Tarea (alta, configuración, recorrido) | El paso en el que estás y su campo. Los demás pasos, plegados |

El número que se persigue **no es el alto de la página**: es qué hay arriba del pliegue. Una lista
larga puede seguir siendo larga —para eso se desplaza dentro de sí misma—, siempre que empiece
arriba.

---

## D2 · El tablero: seis aspectos, sin gráfico ni listas

La primera vista es una grilla de **seis tarjetas de aspecto**, cada una con una cifra y su
denominador. Sin gráfico y sin listas: son detalle, y el detalle es bajo demanda.

| Tarjeta | Cifra | Cómo se abre el detalle |
|---|---|---|
| Cobertura | «92 de 120 con dato» | **Panel** — el reparto por estado de la cuenta, con enlace por dominio |
| Dominios | «3 · 1 pide acción» | **Navega** a `/domains` |
| Cupo de hoy | «412 de 2.000» | **Panel** — los dos bolsillos, por dominio |
| Actividad | «5 lotes hoy» | **Panel** — los últimos lotes, con enlace a cada ficha |
| Sitemaps | «4 registrados» | **Panel** — por dominio, con su última sincronización |
| Avisos | «1 sin leer» | **Navega** a `/notifications?unread=1` |

Arriba de la grilla, la línea de estado (`PageIntro`): «Todo en orden» cuando no hay nada que hacer,
o el bloque de atención con **el único botón primario de la pantalla** cuando lo hay. Al pie, el
sello de zona horaria. Nada más.

Los tres casos de la vista que ya estaban decididos en `views-home.md` se conservan: cuenta sin
dominios, cuenta con dominios y sin URLs, y cuenta en régimen. En los dos primeros la grilla no se
rinde: se rinde un `EmptyState` con una sola acción.

---

## D3 · Panel para lo que no tiene pantalla propia; navegación para lo que sí

- **Navega** lo que ya es una pantalla: dominios, avisos, la cobertura de un dominio, la ficha de un
  lote. Duplicar ese contenido en un panel sería mantener dos diseños de lo mismo, que es el problema
  que este rediseño viene a resolver.
- **Panel lateral** para lo que **no** tiene pantalla propia: los agregados de cuenta —cobertura,
  cupo, actividad, sitemaps— que hoy sólo existen por dominio.
- **Nunca un modal con una tabla adentro.** Para volver a mirar el tablero habría que cerrarlo.

El panel es `DetailSheet`, con su estado en la dirección (`?panel=coverage`), así que es enlazable y
el botón Atrás lo cierra. Abajo de `md` se presenta como `Drawer`: es el mismo componente.

---

## D4 · El harness de tests de frontend se queda

`tests/frontend/` sobrevive y las vistas del rediseño se pueden apoyar en él. Es la única forma de
defender lo que se **dibuja** —que la barra de un lote incompleto no llegue al borde, que un badge
diga icono + palabra— porque ningún test de Django llega más allá de las props.

---

## D5 · Una sola puerta a Google en el menú

El grupo Google ofrecía «Recorrido guiado» y «Conexión» como entradas hermanas, sin decir en qué se
diferencian, y quien no lo sabe elige mal. **Queda una sola entrada: «Conexión con Google» →
`/settings`.**

- **El recorrido no desaparece.** Conserva su URL, sus rutas de comprobación por paso y su progreso;
  se llega desde adentro de Configuración. Los pasos 5 a 7 —dar de alta un dominio, registrar un
  sitemap, correr la primera sincronización— viven en otras pantallas, así que absorberlo dentro de
  Configuración habría hecho que la pantalla de la credencial fuera dueña del alta de dominios.
- **Configuración deja de dibujar su copia de los pasos.** Hoy los siete pasos están escritos una
  sola vez en el servidor y **dibujados tres veces**, con dos componentes en el recorrido y un
  tercero en Configuración, recortados a cuatro dentro de un acordeón. En el estado sin conectar, en
  su lugar va un botón «Guiame paso a paso» que lleva al recorrido al paso que falta.
- **Cambiar entradas, rótulos y destinos del menú está permitido**: lo autoriza por escrito el aviso
  de `app-sidebar.tsx`, que sólo prohíbe cambiar su anatomía.

**Esto además cierra el caso que quedaba abierto:** la primera pantalla del producto mostraba un
banner «Conectar Google → Configuración» encima de un recorrido cuyo paso 3 es exactamente eso. Con
una sola puerta, el aviso y la pantalla dejan de empujar en direcciones distintas.

---

## D6 · El panel de acceso de la ficha de dominio, revisado por el owner

Encontrado mirando la pantalla, sobre una vista que todavía no se rediseñó. **Lo resuelve la fase 4**,
y no con un parche: los tres problemas son de dónde vive cada cosa.

> «Acá no se entiende nada, hay dos botones de "comprobar"; y el error dice "no pudimos completar la
> acción" y lo primero que se me viene a la mente es "¿cuál acción?"»

**1 · Dos botones para la misma acción.** `Domains/Show.tsx:350` («Volver a comprobar») y `:426`
(«Comprobar acceso») disparan **el mismo formulario**. Uno está adentro del bloque de error y el otro
al pie del panel. Queda **uno solo**: el del error desaparece y el del pie es el único, porque la
acción no cambia según haya fallado o no.

**2 · El título genérico es el síntoma de un código sin mapear.** «No pudimos completar la acción» es
`UNMAPPED_TITLE` de `ServerErrorNotice`, que se usa cuando el código no está en el mapa cerrado. El
mapa de la ficha (`ACCESS_ERRORS`) cubre permiso denegado, propiedad inexistente y Google sin
responder; el que aparece es **clave rechazada**, que no está. De ahí que el mensaje de abajo sea
específico —lo escribe el servidor— y el título de arriba no diga nada.

**3 · Y ese error no es de este dominio: es de la cuenta.** Una clave rechazada no se arregla en la
ficha de un dominio sino en Configuración, así que mapear el código ahí sería explicar mejor un
problema que igual está en la pantalla equivocada. Es el mismo patrón que la fase 0 arregló en los
avisos: **un problema se muestra donde se puede resolver.**

Lo que corresponde: la ficha distingue **lo que es del dominio** —permiso, forma de la propiedad,
Google sin responder— de **lo que es de la cuenta**. Lo primero se explica y se acciona ahí; lo
segundo se dice en una línea con su enlace a Configuración, sin ofrecer un botón de comprobar que va
a volver a fallar por el mismo motivo. `CredentialErrorPanel` ya tiene el mapa cerrado de los códigos
de Google, incluido éste: la ficha tiene que apoyarse en él en vez de mantener un tercer mapa.

---

## D7 · El estado de la cuenta es una luz en la barra, no un bloque en cada pantalla

> «Perfectamente podrías tenerlo en el header arriba a la derecha con icono danger, persistente en
> cada vista. De tal manera que si abrís te despliega el popover con la data tal cual como está ahí.
> No tiene por qué estar presente todo el tiempo. Para mí debe ser crucial **no** ver el icono de
> danger en la barra.»

> «Este otro también debería estar en el mismo sitio debajo, como una lista de notificaciones, y en
> este caso un alert variant warning sin ese footer. Así salen los dos de la vista, me dejan mis
> tarjetas, y la lista de críticos y warnings queda siempre visible pero no imprudentes.»

El aviso de nivel cuenta deja de rendirse como bloque dentro del contenido y pasa a ser un
**indicador fijo en el extremo derecho de la barra superior**, que se abre en `Popover` con la lista
completa: título, mensaje y su acción, cada aviso como `Alert` con el tono de su nivel.

**La lista es una sola y mezcla niveles.** Adentro va lo crítico —la conexión con Google, las
propiedades que dejamos de poder leer— y abajo lo que puede esperar: los dominios sin autorizar
todavía y los lotes que fallaron desde ayer. Eso último venía de la tarjeta de estado del tablero,
que **se elimina**: contaba lo mismo en otro lugar y con otra forma.

Para que la lista pudiera juntarlos, las dos familias nuevas pasaron a ser avisos de cuenta en el
servidor (`DOMAINS_AWAITING_ACCESS` y `BATCHES_FAILED`, las dos en `warning`). Tenían que salir de
ahí: el indicador vive en el armazón y se dibuja en las trece pantallas, así que su contenido no
puede depender de las props de una vista.

**El tablero se queda con sus tarjetas y nada más.** Sin la tarjeta de estado, sin su bajada y sin su
pie —«Último control de acceso: …» se va con ella—. La región viva de «Trabajo en curso» sube al slot
de acciones del encabezado: es un **estado** y no una acción, así que no contradice F8, y abajo
habría necesitado un renglón entero para no decir nada casi siempre.

Es el criterio de [F7](./owner-findings.md) llevado al armazón: **la ausencia del icono es la señal
de salud**. Un lugar fijo que casi siempre está vacío se aprende en dos días; un bloque rojo de
120 px que aparece en el medio del contenido se aprende a ignorar en dos días.

**Qué se lleva puesto, y es la mejor parte.** Toda la maquinaria de supresión —`leadsToThisPage`,
`solvedRightHere` y sus dos docstrings largos— existía **sólo** porque un bloque intrusivo tenía que
callarse en las pantallas donde estorbaba. Un icono no estorba en ninguna. **El indicador se muestra
siempre que haya un aviso, sin excepciones, incluida la pantalla que lo resuelve**: una luz de estado
que se apaga en una pantalla es una luz que miente. Con eso también se cae el problema 2 de F12 —el
bloque rojo en Sesiones y Claves de API—, sin necesidad de una regla nueva.

### Las cuatro condiciones

1. **Icono + palabra, nunca el icono solo.** Es RT-04, y es el mismo matiz que ya se aceptó en F7.
   Va el icono más «Revisar», y el nombre accesible dice cuántos avisos hay y de qué. Un glifo suelto
   de 16 px en rojo no lo lee quien no distingue el rojo, y quien nunca vio el estado sano tampoco
   sabe si es un botón o un adorno. **Sin número**: la barra lateral ya tiene un contador —los avisos
   sin leer— y dos contadores cerca se leen como el mismo.
2. **Posición fija, y sola.** El extremo derecho de la barra pasa a ser **siempre** el estado de la
   cuenta y nunca otra cosa; las acciones de cada pantalla quedan a su izquierda, separadas. Si el
   indicador se corre según cuántos botones tenga la vista, deja de aprenderse dónde mirar, que es
   todo lo que este cambio compra.
3. **`Popover` y no `DropdownMenu` ni `Sheet`.** Lo que se abre no es un menú de comandos ni una
   lista larga: son uno o dos avisos con su botón. Si algún día fueran muchos, la pieza correcta pasa
   a ser el panel.
4. **Queda una deuda abierta hasta la fase 3.** El bloque existía para impedir que alguien leyera
   cifras viejas como si fueran de hoy. Encogerlo a un icono saca esa protección **salvo que el dato
   mismo diga que está viejo**, y hoy no lo dice: es R-F, la regla que no se cumple. Se implementó
   igual, a pedido del owner, así que **mientras tanto Cobertura, Sitemaps y Lotes muestran cifras
   viejas sin marca**, con el indicador de la barra como única advertencia. `DomainIdentity`, en la
   fase 3, es lo que lo cierra — y por eso esa fase deja de ser conveniente y pasa a ser deuda.

### Sobre tocar el armazón

`site-header.tsx` prohíbe cambiar su anatomía «sin pedirlo» y pide preguntar antes. **Esto es el
permiso**, y queda anotado en el propio archivo: el extremo derecho es un slot deliberado y con
dueño, para que un agente futuro no lo «limpie» por parecerle de más.

---

## D8 · La página tiene encabezado propio, y la barra de arriba es sólo navegación

> «Esos breadcrumbs están bien, pero la página no tiene título como tal: literalmente está vacía de
> heading. Necesitamos una sección de heading donde haya un slot de actions. De esta manera me sacás
> los botones del header nav y me los ponés en el slot de la página. Que el heading title tenga su
> propio CSS en componente estándar, para que las vistas sólo se preocupen por el content.»

La forma de toda pantalla pasa a ser:

```
[barra: dónde estás]                        [estado de la cuenta]
[título de la página]                       [acciones de la página]
[bajada de la página]
[contenido]
```

**El `<h1>` baja al contenido.** La barra decía el nombre de la vista y era el único encabezado del
producto, así que las trece pantallas no tenían ninguno propio: la columna de contenido empezaba en
una tabla, en un aviso o en un párrafo suelto. Un localizador de navegación no es el titular de la
pantalla.

**Las acciones bajan con él.** Un botón en la barra de navegación se lee como una acción del
producto y no de la pantalla — era «Agregar dominio» flotando arriba a la derecha, lejos de la lista
sobre la que actúa, que es lo que había marcado F8.

**Lo dibuja `PageHeading` y lo rinde `AppLayout`**, con las mismas medidas para las trece: la vista
pasa título, bajada y acciones, y no elige tamaños. Ahí estaba el origen de las siete formas
distintas de dibujar el mismo nivel.

### Lo que esto cambia del contrato

La regla de T1 decía «sólo lo escribe `SiteHeader`; **ninguna página rinde su propio `<h1>`**», y
queda al revés: el `<h1>` es de la página y la barra no lleva ninguno. El tamaño también cambia
—era el de un localizador— y pasa a `text-xl font-semibold`, un escalón por encima de T3 y por
debajo de T2, para no competir con la cifra que contesta la pregunta de la vista.

---

## D9 · Un estado en vivo dice qué y desde cuándo, y no se afirma si no puede ser cierto

> «En Inicio, ¿qué se supone que debo pensar de "Trabajo en curso"?»

Un indicador de actividad tiene que nombrar **qué** trabajo y **desde cuándo**. «Trabajo en curso» a
secas no se puede contradecir: un lote encolado hace dos días que ya no avanza se lee igual que uno
que arrancó recién.

**Dónde vive**: un icono en la barra superior, a la izquierda del estado de la cuenta, con el detalle
en su tooltip — no un badge con texto compitiendo con el título de la página. Es D10: la señal está
siempre a la mano y en el mismo lugar, sin estar en primer plano. Viaja como prop compartida
(`active_work`), igual que los avisos, porque el indicador se dibuja en las trece pantallas.

Y no se afirma cuando no puede ser cierto: **con `can_operate` en falso ningún lote avanza**, así que
ahí no hay trabajo en curso sino trabajo trabado, y quien lo explica es el indicador de la cuenta. Lo
mismo vale para el sondeo: con la conexión caída, `useBatchPolling` no arranca.

**Aplica a**: cualquier vista con estado en vivo — fase 4B (V7 Cobertura) y 4C (V9 Lotes, V10 Ficha
de lote), que sondean con el mismo mecanismo.

**La trampa**: «estado no terminal» no es «está corriendo». Es la condición que había y es la que
producía la contradicción — el tablero decía 0 lotes hoy y 0 consultas hoy mientras afirmaba que
había trabajo.

---

## D10 · Todo a la mano no es todo en primer plano

> «Tenemos que dejar en la pantalla todo a la mano, pero eso no significa todo en primer plano. Es
> algo que necesito que entiendas.»

**Es la regla que gobierna a las demás.** Nada se borra por ocupar lugar: se **baja de plano**. Lo
que estaba gritando pasa a estar a un clic, y sigue estando siempre en el mismo lugar.

Por eso el aviso de cuenta no desapareció —se volvió una luz que se abre (D7)—, y el trabajo en curso
tampoco —se vuelve un icono con su tooltip—. Sacar información es una decisión aparte y se justifica
por sí sola: que algo moleste en primer plano **no** es un argumento para borrarlo.

**Aplica a**: todas las fases. Antes de sacar algo de una vista, la pregunta no es «¿molesta?» sino
«¿a qué plano baja y cómo se llega?».

---

## D11 · Un filtro es un desplegable, no una fila de botones

> «Los filtros son inescalables. Son botones en primer plano, es puro ruido. Deberían ser combo box.
> Y si es un filtro que puede crecer en opciones, entonces un search combobox con un max-height.»

Todo eje de filtro es **`FilterSelect`**: etiqueta arriba, disparador que dice lo que está puesto, y
la lista al desplegar. Nunca una fila de opciones en primer plano — no escala, y con nueve opciones
la región de filtros pesa más que la tabla que filtra.

Lleva buscador cuando el eje **puede crecer**, y eso se declara (`searchable`), no se deduce del
largo: los estados de un lote son cinco y van a seguir siendo cinco; una lista de dominios no tiene
techo. Por omisión se prende solo pasadas las ocho opciones.

**Aplica a**: fases 4A, 4B y 4C — dominios, cobertura, sitemaps y lotes tienen ejes de filtro.
`FilterToggleGroup` y `FilterPopover` **ya no existen**; `FilterChips` sigue, ahora sobre esta pieza.

**La trampa**: las opciones tienen que seguir siendo `<Link>` de verdad, no un `onSelect` que
navegue. El recorte vive en la dirección, y un manejador de clic no se copia, no abre en otra pestaña
y no lo deshace el botón Atrás. Va `CommandItem asChild` sobre el enlace.

**Falta el de selección múltiple.** Comparte la forma —etiqueta, disparador, lista con buscador— y
cambia la semántica: varios valores en la dirección, casillas en vez de marca, y un disparador que
resume en vez de nombrar.

---

## D12 · Un valor enumerado viaja en minúscula y con guiones

> «¿Qué es esa tontería, un snake case en mayúscula?» —sobre `kind=BATCH_FINISHED` en la dirección.

En la base son `TextChoices` de Django y ahí la convención es `BATCH_FINISHED`; en la dirección eso
se lee como una variable escapada de un archivo de código. La forma pública es **minúsculas con
guiones**: `kind=batch-finished`, `access_state=access-lost`, `group=with-data`.

La traducción vive en `apps/core/tables.py` —`slug()`, `from_slug()`, `filter_options()`— y su mitad
del cliente en `frontend/lib/filters.ts`. **No se tocan los modelos**: cambiarlos obligaría a migrar
diez tablas para arreglar un detalle de presentación.

**Aplica a**: todas las fases. Vale para **filtros**, no para campos de formulario — un `<select>`
que postea manda el valor de base, y slugificarlo rompe el alta.

**La trampa**: falla en silencio. Si el valor que dibuja el control y el que el servidor sabe leer no
coinciden, el filtro se descarta y la lista vuelve completa sin que nada avise. Por eso las opciones
las arma `filter_options()` y nunca la vista a mano. Y las claves del JSON —`coverage.with_data`—
**no** son valores de dirección: siguen en snake_case.

---

## D13 · A la guía se llega siempre, no sólo la primera vez

> «Necesito en actions el botón de guía para ir a la guía. Entiendo que hoy sólo se muestra la
> primera vez, pero lo necesitamos siempre, porque es algo que siempre se olvida.»

Un procedimiento que sólo se ofrece en el estado vacío deja de existir justo cuando se necesita: lo
que se olvida no es cómo empezar, es en qué pantalla de Google Cloud estaba cada cosa, y eso se
pregunta con la credencial ya cargada. Todo instructivo va en el slot `actions` del encabezado, en
`ghost`, presente en **todos** los estados de la vista.

**Aplica a**: fases 3, 4A y 4B. Autorizar la cuenta de servicio en Search Console y comprobar el
acceso de un dominio son procedimientos de la misma clase: se hacen afuera y se olvidan igual.

**La trampa**: es un atajo de consulta, no el verbo de la pantalla. En `ghost` y último en peso,
nunca `default` — si compite con la acción que sí resuelve el estado, lo que se pierde es la acción.

---

## D14 · El producto es una app de herramientas, y ésta es la primera

> «Ya no es una sola herramienta sino una app de "Tools", el index-relay pasa a ser una de las
> tools. Todo esto debe estar bajo un solo grupo "Search Console". El nombre index-relay desaparece
> de la interfaz y queda sólo en memorias y contexto.»

La marca visible de la plataforma es **«Tools»**; la de esta herramienta, **«Search Console»**.
«IndexRelay» no aparece en ninguna pantalla. Sus direcciones cuelgan de `/search-console/`, la raíz
pasó a ser el launcher, y el menú tiene dos grupos: la herramienta y la cuenta.

**Aplica a**: todas las fases que queden. Una pantalla nueva de Search Console va bajo el prefijo y
bajo ese grupo; nada se agrega al menú sin decidir antes de qué herramienta es.

**La trampa**: los nombres de ruta **no** llevan el prefijo, sólo las direcciones. `route('domains')`
sigue siendo `domains`. Prefijar el nombre no compra nada hasta que dos herramientas publiquen una
pantalla homónima, y cuesta treinta llamadas del frontend. La API pública tampoco se prefija: está
versionada y ya integrada desde un pipeline de despliegue.
