# Estado del proyecto

**Fecha**: 19 de agosto de 2026
**Rama**: `rediseno-de-la-interfaz` (22 commits; `main` sin tocar)

| Comando | Resultado |
|---|---|
| `uv run pytest -q` | **515 pasan** |
| `uv run ruff check .` | limpio |
| `uv run ruff format --check .` | limpio |
| `npx tsc -b --force` | sin errores |

---

## Qué está construido

**Las trece vistas.** V0 tablero · V1 login · V2 conexión · V3 recorrido guiado · V4 dominios ·
V5 alta · V6 ficha · V7 cobertura · V8 sitemaps · V9 lotes · V10 ficha de lote · V11 claves de API
· V12 sesiones. Todas con ruta publicada y alcanzables desde el menú. Los 18 componentes
compartidos del checklist, también.

**La navegación completa**: tres grupos con rótulo —Herramientas (Inicio, Dominios,
Notificaciones), Google (Recorrido guiado, Conexión), Sistema (Claves de API)— más las sesiones
dentro del menú de la cuenta. Las cuatro vistas de un dominio se alcanzan desde su ficha.

**Historias de usuario**: US1 credenciales, US2 dominios/cobertura/cupo, US3 sitemaps y
sincronización por API, US4 recorrido guiado. US5 parcial (ver abajo). US6 sin empezar.

**Avisos dentro de la plataforma** (`apps/notifications/`), con el número al lado de la entrada del
menú. Ocho tipos: lote terminado, lote fallido, acceso perdido, credencial caída, exportación
lista, y los tres cambios de cobertura que piden una acción —URLs que dejaron de estar indexadas,
URLs que Google no pudo descargar y URLs bloqueadas por `robots.txt`—, cada uno con un enlace a la
cobertura ya filtrada por su estado. Los correos quedaron **fuera de alcance por decisión del
owner**; la tabla está lista para que se apoyen en ella cuando se decida.

**El ciclo se puede forzar a mano** sobre un dominio: `manage.py run_daily_cycle <hostname>` hace
los tres pasos en el orden del ciclo automático y cuenta qué pasó en cada uno.

**La exportación de cobertura, en sus dos formas.** Por debajo de `EXPORT_THRESHOLD` el CSV sale en
la misma respuesta; por encima se encola un lote `COVERAGE_EXPORT` que escribe el archivo
informando avance, avisa al terminar y lo conserva siete días. El archivo no cuelga de ninguna
dirección pública: lo sirve una vista que comprueba de quién es.

**El ciclo diario**: revalidación de acceso a las 2, sincronización a las 3, inspección a las 4,
purga de sesiones a las 5 y de archivos de exportación a las 5:30, en la zona de presentación.

**Todo el código en inglés.** Identificadores, claves de props, claves del JSON guardado en la
base, valores de querystring, nombres de restricciones e ids del DOM. Los comentarios, los
docstrings y los textos de interfaz siguen en español, que es el idioma del producto.
`CLAUDE.md` fija la regla y no admite excepciones.

---

## Qué falta

### Lo grande

1. **US6 — el panel interno.** Sin empezar. Sólo `accounts/admin.py` tiene contenido; los otros
   nueve están vacíos. Falta el registro de auditoría (`AuditLog`), la revocación de acceso desde
   el panel y el cierre de sesiones de una cuenta.

### Infraestructura y verificación

2. `README.md` (T134), `docs/operations.md` (T135) y el ejemplo de integración a un pipeline
   (T097). La instalación en contenedores ya está: `docker compose up` levanta db, redis, web,
   worker y beat, y quedó probada de punta a punta —ingreso, assets sin Vite, y una tarea
   encolada desde la web ejecutada por el trabajador—.
3. Tests de contrato que faltan: dominios (T060), comprobación de acceso (T061), sitemaps (T086),
   idempotencia del sync (T087), lotes con cupo agotado (T110).
4. El test de paridad interfaz/API (T129) y la comparación del esquema generado contra
   `openapi.yaml` en integración continua (T130).
5. **`GET /dashboard` en la API** (T143): el agregado por cuenta sólo lo tiene la interfaz.

### Anotado y no resuelto

- **El tope de URLs monitoreadas del plan se calcula y se descarta** en
  `apps/sitemaps/services.py::_register_urls`: `limits.check()` devuelve una decisión que ahí nadie
  mira, mientras que el alta de dominio sí la respeta. Con la facturación apagada no se nota; el
  día que se encienda, un sync puede pasarse del límite.
- **`summary['errors']` mezcla dos cosas**: intentos que fallaron y notas de un lote que no falló
  nada (cupo agotado, URLs ajenas descartadas). La pantalla lo desambigua mirando `failed_items`,
  pero sería más limpio que el servidor marcara cada entrada con su naturaleza.
- **Demos del bloque sin usar**: `data-table.tsx` y `login-form.tsx` quedaron instalados y no se
  consumen. `section-cards.tsx` y `chart-area-interactive.tsx` sí se usan en el tablero.
- **`sep-test.html`** y **`.mcp.json`** en la raíz, sin commitear. Son del owner.

---

## Cómo trabajar sobre esto

**El armazón no se reestructura.** `AppLayout.tsx`, `app-sidebar.tsx`, `nav-*.tsx` y
`site-header.tsx` llevan arriba un aviso que lo dice. La disposición viene del bloque
`dashboard-01` y la eligió el owner. Ya se rompió dos veces por reescribirla «mejor».

**El encabezado es una franja de alto fijo alineada al centro.** Lo que se le pase como `actions`
tiene que ser de una línea: un badge con su fecha, o un botón con su explicación, se le salen por
arriba y quedan cortados.

**La navegación se dibuja desde las rutas publicadas.** `hasRoute()` filtra cada entrada, así que
una pantalla sin ruta no aparece en el menú. Publicar una ruta implica tocar los tres archivos que
el test de rutas mantiene sincronizados: `config/urls.py`, `apps/core/routes.py` y
`frontend/lib/routes.ts`.

**Las direcciones se resuelven por nombre, nunca se arman.** `route()` en el frontend, `reverse()`
en el servidor. Una cadena armada a mano sigue funcionando el día que la ruta cambia: lleva a un
404 y no hay nada que avise.

**Lo que contesta `202` encola de verdad.** La petición crea el lote en `QUEUED` y la tarea lo toma
**sólo si sigue en cola** —Celery reconoce al terminar, así que una tarea que muere vuelve—. Si
agregás otro trabajo asíncrono, seguí ese patrón: `queue_inspection()` y `queue_sync()` son los
ejemplos.

**El día se corta en la zona de presentación**, no en la del servidor. Son distintas, y en la
franja horaria en que no coinciden «el cupo de ese día» es el de otro día.

**Los avisos salen de donde ocurre el hecho**, no de quien lo dispara: el cierre del lote, la
caída del acceso, el cambio de estado de la credencial. Así valen igual para la pantalla, la API y
el ciclo automático. Y `notify()` no escribe dos veces el mismo hecho: la unicidad la impone la
base, porque un aviso duplicado por una tarea reintentada es el caso que el código olvida.

**La suite no ejecuta las tareas de Celery en el momento**, aunque el `conftest.py` lo pida:
pytest-django carga la configuración antes que ese archivo, así que `CELERY_TASK_ALWAYS_EAGER`
queda en falso y `delay()` intenta hablar con la cola de verdad. Todo test que ejercite trabajo
encolado sustituye `delay` —o llama a la tarea a mano—, como hacen `test_scheduled_work.py` y
`test_coverage_export.py`. Un test que confíe en el modo inmediato falla con un error de conexión
que no menciona a Celery por ningún lado.

**El orden del ciclo de inspección lo decide `priority_score`, y es una política, no un detalle.**
Nunca consultadas primero, después las problemáticas, después las más viejas. Cambiar el mapa de
puntajes cambia qué parte del sitio tiene dato fresco: no es una constante de ajuste fino.

**Al medir en el navegador, cuidado con `querySelector`.** Devuelve el primero que coincide. Una
medición sobre el elemento equivocado costó un diagnóstico entero y una acusación injusta a Vite.

**Repartir trabajo entre agentes**: fijá la tabla de equivalencias o el contrato **antes** de
repartir, y dales una condición de entrega verificable (tests de su zona en verde, no «revisé»).
Lo que se rompe siempre son las costuras: un componente compartido cuya firma cambia después de
que sus consumidores terminaron. `tsc` las encuentra todas; la suite, no necesariamente.

---

## Entorno

- Base de datos: **SQLite** (`db.sqlite3`) al trabajar en la máquina, por el `default` de
  `DATABASE_URL`. La instalación en contenedores corre sobre **PostgreSQL 18**, que es lo que
  exige producción: las diferencias entre las dos se ven ahí y no en la suite.
- **En contenedores**: `cp .env.docker.example .env.docker`, completar los dos secretos y
  `docker compose up --build`. Queda en `http://127.0.0.1:8001` —el 8000 lo suele ocupar el
  servidor de desarrollo— y no comparte base ni cuentas con la instalación local.
- Los archivos de exportación caen en `var/exports/` (`EXPORT_ROOT`), que no se versiona. La
  retención son siete días y la aplica `purge_exports`, a las 5:30 de la zona de presentación.
- `.env` con `DJANGO_VITE_DEV_MODE=true`. Requiere `npm run dev` corriendo.
- Los servidores se levantan a mano. `npm run dev` levanta los dos juntos —Django y Vite, con su
  salida rotulada— y `npm run dev:backend` / `npm run dev:frontend` levantan uno solo. Si uno se
  cae, el otro se cierra con él: un Vite huérfano sirviendo assets sin servidor detrás se parece
  demasiado a que la aplicación anda.
- Cuenta de prueba: `prueba@ejemplo.test` / `prueba-local-1234`.
- El dominio `ejemplo.com` de la base de desarrollo tiene lotes sembrados en los cinco estados y
  actividad repartida en el último mes, para poder mirar V0, V9 y V10 sin esperar un ciclo.
- La suite corre las tareas de Celery en el momento (`CELERY_TASK_ALWAYS_EAGER` en `conftest.py`),
  así que no hace falta Redis para probar.
