---

description: "Task list for 001-gsc-sitemap-coverage"
---

# Tasks: Sincronización de sitemaps y monitoreo de cobertura de indexación

**Input**: Design documents from `/specs/001-gsc-sitemap-coverage/`

**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
[data-model.md](./data-model.md), [contracts/openapi.yaml](./contracts/openapi.yaml)

**Tests**: incluidos. No son opcionales: el apartado *Flujo de Desarrollo y Quality Gates* de la
constitución los exige en contrato de API, integración con dobles de prueba e idempotencia, y
varios invariantes del modelo sólo son verificables por test.

**Organization**: agrupadas por historia de usuario para poder implementar, probar y desplegar
cada una por separado.

## Format: `[ID] [P?] [Story] Description`

- **[P]**: puede ejecutarse en paralelo (archivos distintos, sin dependencias pendientes)
- **[Story]**: historia a la que pertenece (US1 … US6)
- Toda descripción incluye la ruta exacta del archivo

## Path Conventions

Proyecto Django único con interfaz Inertia: `config/` para la configuración, `apps/` para las
aplicaciones por dominio funcional, `frontend/` para las páginas de React y `tests/` en la raíz.
Estructura completa en [plan.md](./plan.md).

---

## Estado al 18 de agosto de 2026

| Fase | Hechas | Verificación |
|---|---|---|
| 1 · Setup | 11 de 11 | T006 escrita; falta construir la imagen (T137) |
| 2 · Foundational | 30 de 30 | ejecutando |
| 3 · US1 — conectar Google | 18 de 18 | en el navegador |
| 4 · US2 — ver la cobertura | 17 de 26 | en el navegador |
| 5 · US3 — sincronizar sitemaps | 7 de 12 | ejecutando |
| 6 · US4 — recorrido guiado | 1 de 12 | sólo el modelo de progreso |
| 7 · US5 — avisos | 0 de 11 | — |
| 8 · US6 — panel interno | 0 de 8 | — |
| 9 · Pulido | 0 de 10 | — |

**Total**: 83 de 138. La suite corre 163 pruebas en verde, `ruff` limpio, `tsc` sin errores,
esquema de la API sin advertencias y `manage.py check` sin incidencias.

Lo verificado en el navegador incluye el camino sin sesión, que es el que faltaba: la raíz y
cualquier ficha mandan al ingreso y después vuelven a la página pedida.

Los hallazgos de la implementación —lo que sólo apareció al ejecutar— están en
[research.md](./research.md), R20.

---

## Phase 1: Setup (Shared Infrastructure)

**Purpose**: dejar el proyecto arrancable, con la cadena de assets funcionando y las herramientas
de calidad en su sitio

**Regla de cierre**: una tarea se marca hecha cuando su comando corre, no cuando el archivo
existe. El andamiaje se genera con las herramientas oficiales, nunca a mano (constitución 1.2.0).

- [x] T001 Generar el proyecto con `django-admin startproject config .` y las doce aplicaciones con `manage.py startapp` bajo `apps/`, corrigiendo el `name` de cada `AppConfig` a `apps.<nombre>` — *verificado: `manage.py check` sin incidencias*
- [x] T002 Resolver las dependencias de Python con `uv add`, fijándolas en `uv.lock` — *verificado: instalación completa; versiones en plan.md*
- [x] T003 [P] Configurar ruff y el formateo en `pyproject.toml` y `.pre-commit-config.yaml`, con el gancho que impide commitear una clave privada — *verificado: `ruff check` limpio y los diez ganchos en verde*
- [x] T004 [P] Generar el frontend con `create-vite -t react` y dejar `package.json` y `vite.config.ts` en la raíz, al estilo Laravel, con la salida a `static/dist` y su manifiesto — *verificado: `npm run build`*
- [x] T005 [P] Instalar Tailwind e inicializar shadcn/ui con su propio CLI (`shadcn init -t vite -b radix -p nova`), dejando los componentes en `frontend/components/ui/` — *verificado: `Button` compila*
- [x] T006 [P] Crear el `Dockerfile` en dos etapas —compilación de assets y ejecución— y el `docker-compose.yml` con db, redis, web, worker, beat y el servidor de Vite para desarrollo — *sin el servicio de Vite: en la imagen los assets ya vienen compilados y en desarrollo Vite corre en la máquina (`npm run dev`), así que un contenedor para él sería una tercera forma de hacer lo mismo. **La imagen todavía no se construyó**: verificarla es T137*
- [x] T007 [P] Crear `.env.example` con las variables de quickstart.md, incluidas `SETTINGS_ENCRYPTION_KEY`, `DISPLAY_TIMEZONE`, `BILLING_ENABLED=false` y `TXT_VERIFICATION_ENABLED=false`
- [x] T008 Implementar la configuración por entorno en `config/settings/base.py`, `dev.py` y `prod.py` con django-environ, declarando la zona horaria de presentación (FR-065) y fallando al arrancar si falta la clave de cifrado — *pendiente dentro de esta tarea: el registro del estado de las capacidades apagadas, que vive en `apps/core/apps.py` (T012)*
- [x] T009 Configurar django-vite e inertia-django en `config/settings/base.py`: middleware de Inertia, `INERTIA_LAYOUT`, y los dos modos de assets distinguidos por configuración — *verificado contra el código de los paquetes instalados*
- [x] T010 Crear `frontend/app.tsx` con la inicialización de Inertia y la resolución de páginas bajo demanda, y `apps/web/templates/base.html` con el bloque que rellena Inertia — *verificado: `GET /login` responde 200 con el componente montado y la hoja de estilos servida*
- [x] T011 [P] Configurar pytest y pytest-django en `pyproject.toml` y `tests/conftest.py`, con un guardia que falle cualquier test que intente salir a la red — *verificado: 7 pruebas en verde*

**Estado**: 11 de 11 escritas. T006 quedó escrita al retomar el despliegue: cinco servicios —db, redis, web, worker y beat—, porque sin worker nada de lo encolado se ejecuta y sin beat el ciclo diario no ocurre. La imagen todavía no se construyó en ninguna máquina; ejecutarla de punta a punta es T137.

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: identidad, cifrado, modelo de credenciales, presupuesto de cuota, cliente de Google,
superficie de API y armazón compartido de la interfaz

**⚠️ CRITICAL**: ninguna historia puede empezar hasta terminar esta fase

- [x] T012 Crear el modelo `Account` en `apps/accounts/models.py` como `AUTH_USER_MODEL`, con su migración
- [x] T013 [P] Crear los modelos `Plan` y `ConsumptionRecord` en `apps/billing/models.py` con su migración, más una migración de datos que cree el plan `unlimited` marcado como predeterminado
- [x] T014 Implementar `limits.check(account, action, amount)` en `apps/billing/limits.py`: con `BILLING_ENABLED` apagado autoriza siempre, pero registra el consumo
- [x] T015 [P] Escribir en `tests/unit/test_deferred_capabilities.py` los tests de que los flags están apagados por defecto y de que `limits.check` se invoca igual estando apagado
- [x] T016 [P] Crear el modelo `ApiKey` en `apps/accounts/models.py` con hash de la clave y prefijo visible, más el comando `apps/accounts/management/commands/create_api_key.py`
- [x] T017 Implementar la autenticación por clave en `apps/accounts/authentication.py` para DRF, con actualización diferida de `last_used_at`
- [x] T018 [P] Escribir en `tests/contract/test_authentication.py` los tests de `401` ante clave ausente, inválida y revocada
- [x] T019 Implementar el ingreso con correo y contraseña en las rutas `/login` y `/logout` —sin prefijo de agrupación—: vista de Inertia en `apps/web/views.py` y página `frontend/pages/Login.tsx`, con el manejo de errores de validación y el redireccionamiento posterior
- [x] T020 Montar el router de la API v1 en `config/api_urls.py` y publicar el esquema y la página navegable con drf-spectacular
- [x] T021 Implementar el manejo de errores en `apps/core/errors.py`: respuestas estructuradas con código estable para la API, y errores de validación y mensajes efímeros para las vistas de Inertia
- [x] T022 Configurar el registro estructurado en `config/settings/base.py` con un filtro en `apps/core/logging.py` que elimine credenciales de los mensajes
- [x] T023 Implementar el cifrado reversible y su rotación en `apps/credentials/crypto.py`, con la clave tomada del entorno
- [x] T024 [P] Escribir en `tests/unit/test_crypto.py` los tests de cifrado y descifrado, de rotación de clave y de que el valor cifrado no contiene el texto original
- [x] T025 Crear los modelos `GoogleProject`, `Module`, `Credential` y `ModuleCredential` en `apps/credentials/models.py` con su migración, más una migración de datos que cree el módulo `SEARCH_CONSOLE`
- [x] T026 Implementar la resolución de credencial por módulo en `apps/credentials/services.py`, sin copiar la credencial en el dominio
- [x] T027 Implementar en `apps/gsc/auth.py` la construcción del cliente autenticado a partir de la credencial resuelta, descifrando en memoria y sin registrar el material
- [x] T028 Crear el modelo `Domain` en `apps/domains/models.py` con su migración y la máquina de estados de `access_state` definida en data-model.md
- [x] T029 [P] Crear los modelos `Batch` y `QuotaBudget` en `apps/jobs/models.py` con su migración y la unicidad `(domain, date)`
- [x] T030 Implementar la reserva y liberación atómica del cupo en `apps/jobs/budget.py` con bloqueo de fila y cupos separados automático y manual
- [x] T031 [P] Escribir en `tests/unit/test_budget.py` los tests de que el cupo no se excede, de que el trabajo automático no consume la reserva manual y de que una reserva no usada se libera
- [x] T032 Implementar `apps/gsc/client.py` como único punto de salida hacia Google, exigiendo reserva concedida en cada llamada y aplicando retroceso exponencial con jitter ante `429` y `503`
- [x] T033 [P] Escribir en `tests/unit/test_gsc_guard.py` el test de que ninguna llamada a Google puede ejecutarse sin reserva concedida
- [x] T034 Configurar Celery en `config/celery.py` con el programador beat y las colas de trabajo
- [x] T035 Implementar el servicio de idempotencia en `apps/core/idempotency.py`, guardando la respuesta asociada a `Idempotency-Key` durante 24 horas
- [x] T036 [P] Grabar en `tests/fixtures/gsc/` las respuestas de ejemplo de Google: inspección de URL, listado de sitemaps, envío de sitemap, permiso denegado, propiedad inexistente y API no habilitada
- [x] T037 Implementar el estado derivado de cuenta y su endpoint `GET /account` en `apps/accounts/services.py` y `apps/accounts/api.py`: credencial vigente, dominios con acceso perdido, zona de presentación y avisos, de modo que ningún dominio pueda mostrarse como operativo si la credencial de la cuenta dejó de servir (FR-064)
- [x] T038 Fijar en `frontend/hooks/` el único mecanismo de actualización del trabajo en curso: consulta cada 5 segundos mientras haya un lote no terminal y la pestaña esté visible, con corte al alcanzar un estado terminal (FR-063)
- [x] T039 Implementar la resolución de rutas nombradas en el frontend según R19: `apps/core/routes.py` serializa las rutas publicadas con una lista explícita de qué se expone, viajan como prop compartida de Inertia, y `frontend/lib/routes.ts` las resuelve con `route(name, params)` tipado por nombre de ruta
- [x] T040 [P] Escribir en `tests/unit/test_routes.py` los tests de que sólo se publican las rutas declaradas, de que el admin y las internas quedan afuera, y de que omitir un parámetro obligatorio produce un error explícito
- [x] T041 [P] Crear el armazón compartido de la interfaz en `frontend/layouts/` y `frontend/components/`: disposición general, navegación, formularios, estados de carga y presentación de errores del servidor

**Estado**: 30 de 30 hechas y verificadas ejecutando.

**Checkpoint**: base lista — las historias pueden empezar

---

## Phase 3: User Story 1 - Conectar Google sin saber qué es Google Cloud (Priority: P1) 🎯 MVP

**Goal**: que alguien que nunca entró a Google Cloud deje la credencial cargada y verificada
siguiendo la guía de la propia plataforma

**Independent Test**: entregar la plataforma a alguien sin experiencia en Google Cloud y verificar
que llega a tener la credencial verificada sin ayuda externa. No requiere dominios ni sitemaps.

### Tests for User Story 1

> Escribir estos tests primero y verificar que fallan antes de implementar

- [x] T042 [P] [US1] Test de contrato de `POST /credentials` y `GET /credentials` en `tests/contract/test_credentials.py`, incluido el `422` cuyo detalle nombra el campo faltante o el tipo hallado
- [x] T043 [P] [US1] Test de contrato de `POST /credentials/{id}/verify` en `tests/contract/test_credential_verify.py`, verificando que `INVALID_KEY`, `API_NOT_ENABLED`, `PROJECT_MISMATCH`, `NO_PROPERTIES` y `PROVIDER_UNAVAILABLE` se distinguen entre sí
- [x] T044 [P] [US1] Test de contrato de `GET /modules` en `tests/contract/test_modules.py`
- [x] T045 [P] [US1] Test de integración del recorrido carga → comprobación → listado de propiedades accesibles en `tests/integration/test_credential_setup.py` — *verificado dentro de `tests/contract/test_credentials.py`: carga, comprobación y listado de propiedades en un recorrido*
- [x] T046 [P] [US1] Test en `tests/unit/test_secret_never_leaks.py` de que el material de la clave no aparece en respuestas de la API, en las props que reciben las páginas de Inertia, en plantillas de correo ni en registros

### Implementation for User Story 1

- [x] T047 [US1] Implementar la validación estructural del archivo de clave en `apps/credentials/validation.py`, con mensajes que nombran el campo faltante o el tipo encontrado y que rechazan explícitamente una API key de Google
- [x] T048 [P] [US1] Escribir en `tests/unit/test_key_validation.py` los tests con los archivos equivocados más frecuentes: API key, credencial de aplicación de escritorio, JSON incompleto y archivo ilegible
- [x] T049 [US1] Implementar la validación de formato del identificador de proyecto en `apps/credentials/validation.py`, sin realizar llamadas externas
- [x] T050 [US1] Implementar la carga de credencial en `apps/credentials/services.py`: valida, cifra el material y deriva `client_email`, `key_fingerprint` y `private_key_id`
- [x] T051 [US1] Implementar la comprobación de credencial en `apps/credentials/services.py` distinguiendo los cinco fallos del contrato y devolviendo el enlace de resolución de cada uno
- [x] T052 [US1] Implementar el reemplazo de credencial en `apps/credentials/services.py`, desactivando la anterior sin modificar ninguna fila de dominios
- [x] T053 [US1] Implementar los endpoints de credenciales y módulos en `apps/credentials/api.py`
- [x] T054 [US1] Escribir el contenido de la guía en `apps/onboarding/content.py`: por cada paso, su objetivo, la ruta exacta dentro de las pantallas de Google y el ejemplo del dato que hay que traer
- [x] T055 [US1] Añadir a `apps/onboarding/content.py` el ejemplo anonimizado del archivo de cuenta de servicio, resaltando `type`, `project_id`, `private_key_id` y `client_email`, sin material secreto real
- [x] T056 [US1] Implementar la vista de configuración en `apps/web/views.py`, armando las props sin lógica de negocio propia, y la página `frontend/pages/Settings/Index.tsx` con el estado de la conexión y la guía embebida
- [x] T057 [US1] Implementar los componentes `frontend/components/GuideStep.tsx` y `frontend/components/KeyFileExample.tsx`, con la comprobación por paso y el ejemplo del archivo resaltado
- [x] T058 [US1] Mostrar en la página de configuración la dirección de la cuenta de servicio con la instrucción de agregarla en Search Console como **propietario**, advirtiendo que un permiso menor no habilita la inspección de URLs
- [x] T059 [US1] Impedir en `apps/domains/services.py` las operaciones sobre dominios mientras el módulo de Search Console no tenga credencial verificada, con un mensaje que enlace a la configuración

**Estado**: 18 de 18 hechas. Verificado en el navegador: la guía de siete pasos con el ejemplo
del archivo, la carga con comprobación inmediata y los cinco fallos distinguidos entre sí.

**Checkpoint**: una persona sin experiencia en Google Cloud puede dejar la plataforma conectada

---

## Phase 4: User Story 2 - Ver la cobertura real de un sitio grande (Priority: P1) 🎯 MVP

**Goal**: dar de alta un dominio, comprobar el acceso a su propiedad, leer sus sitemaps para
descubrir URLs e inspeccionarlas, y mostrar el estado real de cada una con su fecha de obtención

**Independent Test**: con una credencial verificada, dar de alta un dominio y confirmar que el
tablero muestra los estados de cobertura fechados

### Tests for User Story 2

- [ ] T060 [P] [US2] Test de contrato de `POST /domains` y `GET /domains` en `tests/contract/test_domains.py`
- [ ] T061 [P] [US2] Test de contrato de `POST /domains/{id}/check-access` en `tests/contract/test_check_access.py`, verificando que permiso denegado, propiedad inexistente y fallo temporal se distinguen
- [x] T062 [P] [US2] Test de contrato de `GET /domains/{id}/coverage` y `GET /domains/{id}/quota` en `tests/contract/test_coverage.py`
- [ ] T063 [P] [US2] Test de integración del recorrido alta → sin acceso → autorizado → operativo en `tests/integration/test_domain_onboarding.py`
- [x] T064 [P] [US2] Test de integración en `tests/integration/test_unknown_urls.py` de que una URL no inspeccionada se reporta `UNKNOWN` con `fetched_at` nulo y jamás como indexada o no indexada — *verificado en `tests/integration/test_sync_and_coverage.py`*

### Implementation for User Story 2

- [x] T065 [P] [US2] Crear los modelos `Sitemap` y `Url` en `apps/sitemaps/models.py` con su migración y los índices de data-model.md
- [x] T066 [P] [US2] Crear el modelo `CoverageRecord` en `apps/coverage/models.py` con su migración y `fetched_at` obligatorio
- [x] T067 [P] [US2] Implementar la traducción de estado en `apps/coverage/mapping.py` como función pura, derivando `INDEXED` sólo ante veredicto `PASS` y todo valor no reconocido a `OTHER_NOT_INDEXED` — *vive en `apps/coverage/translation.py`, no en `mapping.py`*
- [x] T068 [P] [US2] Escribir en `tests/unit/test_coverage_mapping.py` los tests del mapeo, incluido que un valor desconocido nunca produce `INDEXED` ni `UNKNOWN` — *en `tests/unit/test_coverage_translation.py`*
- [x] T069 [US2] Implementar la lectura de sitemaps en `apps/sitemaps/parser.py`: índice y conjunto de URLs, contenido comprimido, límite de 50.000 entradas y descarte de URLs de otro dominio — *vive en `apps/sitemaps/reader.py`, no en `parser.py`*
- [x] T070 [P] [US2] Escribir en `tests/unit/test_sitemap_parser.py` los tests del análisis con esos formatos y casos límite — *en `tests/unit/test_sitemap_reader.py`*
- [x] T071 [US2] Implementar el alta de dominio en `apps/domains/services.py`: normalización, validación del `property_uri` según su forma e invocación de `limits.check`
- [x] T072 [US2] Implementar la comprobación de acceso en `apps/domains/services.py`, distinguiendo permiso denegado, propiedad inexistente y error temporal
- [x] T073 [US2] Añadir el método de inspección de URL a `apps/gsc/client.py`, devolviendo los campos crudos de data-model.md
- [x] T074 [US2] Implementar en `apps/coverage/services.py` la inspección de una URL: reserva de cupo, persistencia de los campos crudos con su `fetched_at`, cálculo del estado derivado y escritura en el historial sólo cuando cambia
- [x] T075 [US2] Implementar en `apps/coverage/tasks.py` la tarea de inspección por trozos que crea el lote, informa progreso y lo cierra en `PARTIAL` si se agota el cupo — *la tarea recibe el lote que la petición dejó en cola, cierra en `PARTIAL` y anota el avance cada 25 URLs, que a un segundo por consulta son unos veinte segundos: la barra avanza a saltos visibles sin una escritura por consulta*
- [x] T076 [US2] Implementar los endpoints de dominios en `apps/domains/api.py`, exponiendo la dirección de la cuenta de servicio a autorizar
- [x] T077 [US2] Implementar `GET /domains/{id}/coverage` en `apps/coverage/api.py` con filtros, conteo por estado y estimación de días del ciclo completo
- [x] T078 [US2] Implementar `GET /domains/{id}/quota` en `apps/jobs/api.py` con el consumo y el saldo de ambos cupos — *vive en `apps/domains/api.py` junto al resto de lo del dominio*
- [x] T079 [US2] Implementar la exportación de cobertura en `apps/coverage/services.py` y `GET /domains/{id}/coverage/export`, con el motivo declarado por Google en cada fila, descarga inmediata por debajo del umbral de filas y lote de exportación con aviso por encima (FR-058) — *el lote `COVERAGE_EXPORT` escribe el archivo informando avance, lo deja siete días y avisa **dentro de la plataforma**: el correo está fuera de alcance por decisión del owner, así que prometerlo habría sido cablear a medias. El archivo no se sirve desde una dirección pública*
- [x] T080 [US2] Implementar `PATCH /domains/{domain_id}` en `apps/domains/api.py` y `apps/domains/services.py` para editar el interruptor de notificaciones y el presupuesto diario, validando que el presupuesto no quede por debajo de la reserva manual (FR-057) — *el error señala el campo que la persona tocó, no siempre la reserva*
- [x] T081 [US2] Añadir el resumen de cobertura fechado al listado de dominios en `apps/domains/api.py` y `apps/coverage/services.py`, siempre con su denominador —URLs con dato sobre el total— y la fecha del último ciclo (FR-062)
- [x] T082 [US2] Implementar el historial de una URL: `GET /domains/{domain_id}/coverage/history` en `apps/coverage/api.py` y el panel lateral de detalle dentro de `frontend/pages/Coverage/Index.tsx`, con la línea de tiempo fechada de cada cambio de estado (FR-060) — *la URL abierta viaja en la cadena de consulta, así que el panel es enlazable y Atrás lo cierra*
- [x] T083 [US2] Implementar el cálculo de `priority_score` en `apps/coverage/services.py`: nunca inspeccionadas, luego problemáticas, luego por antigüedad — *el puntaje se escribe en cada lectura y describe el estado de hoy, no la historia de la URL. El orden del ciclo cambió: el puntaje va **antes** que la antigüedad, porque detrás de ella sólo desempataba entre lecturas del mismo instante y no se aplicaba nunca. Las URLs que ya tenían estado quedaron con su puntaje por migración*
- [x] T084 [US2] Implementar las vistas de dominios y cobertura en `apps/web/views.py` y las páginas `frontend/pages/Domains/` y `frontend/pages/Coverage/`, con el tablero por estado, los filtros y la fecha de obtención visible en cada fila
- [x] T085 [US2] Implementar el comando `apps/coverage/management/commands/run_daily_cycle.py` para forzar un ciclo sobre un dominio — *los tres pasos en el orden del ciclo automático, y corta donde él corta: sin acceso confirmado no sigue. Hace el trabajo en el momento y lo cuenta; con `--queue` lo deja en la cola. Consume el cupo automático y no la reserva manual, porque reemplaza al trabajo del ciclo en vez de agregarse*

**Estado**: 20 de 26. El recorrido central funciona de punta a punta —alta de dominio,
comprobación de acceso, lectura de sitemaps, inspección con cupo y tablero fechado— y está
verificado en el navegador. Quedan afuera: los tests de contrato de dominios y de comprobación
de acceso (T060, T061, T063), la mitad grande de la exportación (T079), el cálculo de prioridad
(T083) y el comando de ciclo (T085).

**Checkpoint**: US1 + US2 son el MVP desplegable

---

## Phase 5: User Story 3 - Que el sitemap se sincronice solo después de cada deploy (Priority: P2)

**Goal**: registrar sitemaps, detectar qué cambió y reenviar sólo eso, desde un endpoint pensado
para el pipeline de despliegue

**Independent Test**: llamar al endpoint de sincronización con una clave de API sobre un dominio
operativo y comprobar que queda registrado el envío y su resultado

### Tests for User Story 3

- [ ] T086 [P] [US3] Test de contrato de `POST /domains/{id}/sitemaps` y `GET /domains/{id}/sitemaps` en `tests/contract/test_sitemaps.py`
- [ ] T087 [P] [US3] Test de contrato de `POST /domains/{id}/sync` en `tests/contract/test_sync.py`, incluido que repetir con la misma `Idempotency-Key` devuelve el mismo lote sin trabajo nuevo
- [x] T088 [P] [US3] Test de integración en `tests/integration/test_sitemap_sync.py` de que un sitemap sin cambios se marca `SKIPPED_UNCHANGED` y no se reenvía — *en `tests/integration/test_sync_and_coverage.py`*

### Implementation for User Story 3

- [x] T089 [US3] Implementar el descubrimiento de sitemaps hijos en `apps/sitemaps/services.py`, registrándolos con `source=DISCOVERED`
- [x] T090 [US3] Implementar en `apps/sitemaps/services.py` el cálculo de `content_hash` y la comparación con la lectura anterior, marcando altas, bajas y modificaciones y poniendo `in_sitemap` en falso para las URLs desaparecidas
- [x] T091 [US3] Añadir el envío de sitemap a `apps/gsc/client.py`, registrando fecha y resultado
- [x] T092 [US3] Implementar la sincronización en `apps/sitemaps/services.py` y `apps/sitemaps/tasks.py`: crea el lote `SITEMAP_SYNC`, envía sólo lo que cambió y arma el resumen de enviados y omitidos
- [x] T093 [US3] Implementar los endpoints de sitemaps y sincronización en `apps/sitemaps/api.py`, aplicando la cabecera de idempotencia — *la idempotencia está cableada en `POST /sync`; falta el test que la ejercita (T087)*
- [x] T094 [US3] Implementar el rechazo explícito con `422` y su motivo en `apps/sitemaps/services.py` para sitemaps inaccesibles, mal formados o con URLs ajenas — *`check_sitemap()` corre en el alta, así que el rechazo llega al pegar la dirección y no al día siguiente. Con URLs ajenas sólo rechaza si **ninguna** pertenece al dominio: que declare algunas es común y la sincronización las descarta con su aviso. El alta no guarda nada de esa lectura, para no dejar una fila que dice cuántas URLs descubrió mientras la cobertura sigue en cero*
- [x] T095 [US3] Implementar la vista de sitemaps en `apps/web/views.py` y la página `frontend/pages/Sitemaps/Index.tsx` con el estado de envío de cada uno
- [x] T096 [US3] Implementar la gestión de claves de API en la ruta `/api-keys`: endpoints en `apps/accounts/api.py` y página `frontend/pages/ApiKeys.tsx`, mostrando el valor completo una única vez al crearla, su fecha de último uso, la acción de revocar y las claves ya revocadas con su fecha, en una sección aparte y sin acciones (FR-061)
- [ ] T097 [US3] Documentar el ejemplo de integración a un pipeline en `docs/pipeline.md` y enlazarlo desde la página de la API

---

## Phase 6: User Story 4 - Recorrido guiado de primera vez (Priority: P2)

**Goal**: hilvanar todo el camino —conectar Google, dar de alta el dominio, autorizar la
propiedad, registrar el sitemap y lanzar el primer lote— con comprobación efectiva en cada paso y
salida al modo experto

**Independent Test**: entrar con una cuenta nueva y llegar al primer lote encolado sin salir del
recorrido; luego abandonarlo a mitad y comprobar que el modo directo funciona y que se retoma en
el mismo punto sin duplicar nada

### Tests for User Story 4

- [x] T098 [P] [US4] Test de contrato de `GET /onboarding`, `POST /onboarding/steps/{step}/verify` y `POST /onboarding/dismiss` en `tests/contract/test_onboarding.py`
- [x] T099 [P] [US4] Test de integración del recorrido completo hasta el primer lote encolado en `tests/integration/test_onboarding_flow.py`
- [x] T100 [P] [US4] Test de integración en `tests/integration/test_onboarding_resume.py` de que abandonar y retomar no duplica proyecto, credencial, dominio, sitemap ni lote
- [x] T101 [P] [US4] Test en `tests/integration/test_onboarding_dismiss.py` de que omitir el recorrido no restringe ninguna funcionalidad

### Implementation for User Story 4

- [x] T102 [US4] Crear el modelo `OnboardingProgress` en `apps/onboarding/models.py` con su migración, con los pasos y el contexto de data-model.md
- [x] T103 [US4] Definir los pasos y su comprobación efectiva en `apps/onboarding/steps.py`, de modo que un paso sólo se marque cumplido si su requisito se verifica
- [x] T104 [US4] Implementar en `apps/onboarding/services.py` el avance, el retomado y la omisión, reutilizando lo ya creado a través del contexto para no duplicar objetos
- [x] T105 [US4] Implementar los endpoints del recorrido en `apps/onboarding/api.py`
- [x] T106 [US4] Implementar la vista del recorrido en `apps/web/views.py` y las páginas `frontend/pages/Onboarding/`, con el asistente por pasos que comprueba sin recargar y reutiliza `GuideStep` — *las tres vistas viven en `apps/onboarding/views.py`, con el resto de lo del recorrido, y no en `apps/web/`*
- [x] T107 [US4] Implementar la detección de primera vez y la oferta del recorrido al ingresar, con opción de omitirlo — *la raíz decide con `should_offer()`; el ingreso pasa por ella en vez de decidir por su cuenta*
- [x] T108 [US4] Implementar en cada paso el mensaje de qué falta y dónde resolverlo cuando la comprobación no pasa
- [x] T109 [US4] Verificar que los formularios completos ofrecen las mismas operaciones en una sola pantalla por objeto, para quien omite el recorrido — *cada paso enlaza a su formulario completo con `form_path`, y omitir no restringe nada*

---

## Phase 7: User Story 5 - Enterarse de los problemas sin entrar a la plataforma (Priority: P3)

**Goal**: ciclo diario automático, lotes con progreso visible y correos al terminar un lote y ante
cambios de cobertura relevantes

**Independent Test**: forzar un ciclo sobre un dominio monitoreado y comprobar que el lote reporta
progreso y que sale un único correo de resumen

### Tests for User Story 5

- [ ] T110 [P] [US5] Test de contrato de `GET /batches/{id}` y `POST /domains/{id}/inspections` en `tests/contract/test_batches.py`, incluido el `429` con cupo agotado
- [ ] T111 [P] [US5] Test de integración en `tests/integration/test_notifications.py` de que un lote terminado envía un único correo y de que reejecutar la tarea no envía un segundo
- [ ] T112 [P] [US5] Test de integración en `tests/integration/test_coverage_change.py` de que una URL que pasa de indexada a no indexada genera el registro histórico y entra en la alerta

### Implementation for User Story 5

- [x] T113 [US5] Crear el modelo `Notification` en `apps/notifications/models.py` con su migración y la unicidad sobre `dedupe_key` — *la unicidad la impone la base y no el código que escribe: un aviso duplicado por una tarea reintentada es el caso que el código olvida y el índice no*
- [ ] T114 [US5] Implementar la composición de correos en `apps/notifications/services.py` y sus plantillas, sin mencionar capacidades apagadas ni prometer indexación — ***fuera de alcance por decisión del owner**: los avisos viven en la plataforma, con su número en el menú. La tabla queda lista para que el correo se apoye en ella cuando se decida*
- [ ] T115 [US5] Implementar la tarea de envío en `apps/notifications/tasks.py` con reintento y registro del estado de entrega — *fuera de alcance junto con T114*
- [x] T116 [US5] Implementar la detección de cambios relevantes en `apps/coverage/services.py`: pérdida de indexación, errores de servidor y bloqueos de rastreo — *los tres se cuentan en el bucle, que es el único momento en que existen a la vez el estado viejo y el nuevo, y salen como tres avisos al cerrar el lote. Son tres y no uno porque se resuelven en tres lugares distintos —el índice de Google, el servidor del sitio y su `robots.txt`—, y cada aviso lleva a la cobertura ya filtrada por su estado. Se cuentan **transiciones**: una URL que viene fallando desde la semana pasada no es una novedad*
- [x] T117 [US5] Implementar la revalidación periódica de acceso en `apps/domains/tasks.py` con su notificación de acceso perdido — *corre a las 2, antes que la sincronización y la inspección: si el acceso se perdió, las dos siguientes no tienen nada que hacer sobre ese dominio*
- [x] T118 [US5] Implementar el interruptor de notificaciones por dominio en `apps/domains/api.py` y en la página de configuración del dominio — *guarda solo, a diferencia de los dos números, que necesitan confirmación*
- [x] T119 [US5] Implementar `GET /batches/{id}` en `apps/jobs/api.py` y la página `frontend/pages/Batches/` con el progreso del lote y el consumo de cupo — *las dos pantallas ya tienen dirección publicada: `/domains/{id}/batches` y `/batches/{id}`*
- [x] T120 [US5] Registrar en `config/celery.py` la programación diaria del ciclo por dominio y de la revalidación de acceso — *revalidación a las 2, sincronización a las 3, inspección a las 4 y purga de sesiones a las 5, en la zona de presentación*

---

## Phase 8: User Story 6 - Operar la plataforma desde adentro (Priority: P4)

**Goal**: panel interno para consultar cuentas y consumo, revocar accesos, cerrar sesiones y
auditar todo lo anterior

**Independent Test**: ejecutar cada acción administrativa sobre una cuenta de prueba y verificar
su efecto y su registro de auditoría

### Tests for User Story 6

- [ ] T121 [P] [US6] Test de integración de las acciones administrativas en `tests/integration/test_admin_actions.py`, incluida la exigencia de motivo

### Implementation for User Story 6

- [ ] T122 [P] [US6] Crear el modelo `AuditLog` en `apps/core/models.py` con su migración
- [ ] T123 [US6] Configurar el panel interno en `apps/accounts/admin.py`, `apps/domains/admin.py`, `apps/credentials/admin.py` y `apps/jobs/admin.py`, mostrando cuentas, dominios, consumo y estado de credenciales sin exponer jamás el material de la clave
- [ ] T124 [US6] Implementar la revocación de acceso de un dominio en `apps/domains/services.py`, dejándolo en `ACCESS_REVOKED` y conservando el historial
- [ ] T125 [US6] Implementar el cierre de sesiones de una cuenta en `apps/accounts/services.py` y su acción en el panel
- [x] T126 [US6] Crear el modelo `Session` en `apps/accounts/models.py` con su migración y registrar por sesión el agente de usuario, la dirección de origen y la última actividad, actualizada de forma diferida (FR-059)
- [x] T127 [US6] Implementar la vista de sesiones activas del propio usuario en la ruta `/sessions`: `apps/web/views.py`, `frontend/pages/Sessions.tsx` y los endpoints de listado y cierre, señalando la sesión en curso y cerrando también en el almacén del framework (FR-002) — *las vistas viven en `apps/accounts/views.py`; probada de punta a punta en el navegador*
- [ ] T128 [US6] Implementar el registro de auditoría en `apps/core/audit.py` y engancharlo a las acciones del panel, exigiendo autor, fecha y motivo

---

## Phase 9: Polish & Cross-Cutting Concerns

- [ ] T129 Escribir el test de paridad entre interfaz y API en `tests/contract/test_ui_api_parity.py`, verificando que toda operación de negocio disponible en las vistas de Inertia existe también como endpoint
- [ ] T130 Añadir a la integración continua la comparación del esquema generado contra `specs/001-gsc-sitemap-coverage/contracts/openapi.yaml`, fallando ante diferencias no declaradas
- [ ] T131 [P] Escribir en `tests/unit/test_hidden_capabilities.py` el test de que el esquema publicado, las props de las páginas, las plantillas de correo y las pantallas no mencionan planes, precios ni verificación TXT
- [ ] T132 [P] Escribir en `tests/integration/test_idempotency.py` los tests de que cada tarea asíncrona ejecutada dos veces deja el mismo estado observable
- [ ] T133 [P] Auditar los textos de `frontend/`, `apps/notifications/templates/`, `apps/onboarding/content.py` y `docs/`, y dejar el resultado como test en `tests/unit/test_no_indexing_promises.py`
- [ ] T134 [P] Escribir `README.md` con el arranque del proyecto, la cadena de assets y la referencia al principio IV sobre la API como superficie de primera clase
- [ ] T135 Documentar en `docs/operations.md` la rotación de la clave de cifrado y el procedimiento ante su pérdida o filtración
- [ ] T136 Revisar las consultas de `apps/coverage/api.py` y `apps/coverage/services.py` para eliminar consultas repetidas y confirmar el uso de los índices de data-model.md
- [x] T137 Verificar la compilación de assets de producción en el `Dockerfile` y que la aplicación arranca sin el servidor de Vite — *construida y levantada: los cinco servicios sanos, las migraciones aplicadas sobre PostgreSQL, ingreso por HTTP con la sesión funcionando, los assets servidos por WhiteNoise desde el manifiesto —con su huella, cacheados para siempre— y una tarea encolada desde la web ejecutada por el trabajador. La imagen final pesa 167 MB y no lleva Node adentro*
- [ ] T138 Ejecutar de punta a punta la validación de [quickstart.md](./quickstart.md) contra un dominio real

---

## Phase 10: Tablero de inicio (V0)

**Goal**: que la raíz conteste «¿hay algo roto?» y «¿cuánto sabemos hoy de mis sitios?» sin entrar
a ningún dominio

Esta fase no estaba en el plan original y la pidió el owner: la raíz redirigía al listado de
dominios, que es un inventario y no un estado. Con seis dominios, saber si algo andaba mal eran
seis viajes. La vista quedó especificada como **V0** en [ux-checklist.md](./ux-checklist.md).

**Independent Test**: abrir la raíz con un dominio sin acceso y comprobar que lo que hay que
resolver aparece antes que cualquier cifra

- [x] T139 [US2] Implementar `apps/web/dashboard.py` con el resumen de la cuenta: lo que necesita acción, la cobertura agregada con su denominador, el cupo del día y la serie de actividad por día, sin estimar ninguna cifra
- [x] T140 [US2] Convertir `home` en `apps/web/views.py` en la vista del tablero, con el rango del gráfico en la cadena de consulta, y dejar la única redirección para quien todavía no conectó su cuenta
- [x] T141 [US2] Construir `frontend/pages/Dashboard.tsx` consumiendo `section-cards` y `chart-area-interactive` del bloque `dashboard-01`, reemplazando sus datos de demostración por los reales
- [x] T142 [US2] Escribir en `tests/integration/test_tablero.py` los tests del tablero, incluido que `UNKNOWN` se cuenta aparte, que un lote `PARTIAL` no pide atención y que la serie no saltea días sin actividad
- [ ] T143 [US2] Exponer `GET /dashboard` en la API con el mismo resumen, para la paridad del principio IV — *hoy los datos existen repartidos en `GET /domains`, `GET /domains/{id}/coverage` y `GET /batches/{id}`; el agregado por cuenta sólo lo tiene la interfaz*

---

## Dependencies & Execution Order

### Phase Dependencies

- **Setup (Fase 1)**: sin dependencias
- **Foundational (Fase 2)**: depende de la fase 1 y **bloquea todas las historias**
- **US1 (Fase 3)**: depende de la fase 2. Es la puerta de entrada: sin credencial verificada no
  hay nada que consultar
- **US2 (Fase 4)**: depende de la fase 2 y, en la práctica, de US1 para tener credencial
- **US3 (Fase 5)**: depende de la fase 2. Reutiliza `Sitemap` y `Url` creados en T065
- **US4 (Fase 6)**: depende de US1, US2 y US3, porque hilvana sus pasos
- **US5 (Fase 7)**: depende de la fase 2 y del modelo `Batch`; sus alertas cobran sentido con US2
- **US6 (Fase 8)**: depende de la fase 2, independiente del resto
- **Polish (Fase 9)**: depende de las historias que se quieran cerrar

### Dentro de cada historia

- Los tests se escriben primero y deben fallar antes de implementar
- Modelos antes que servicios, servicios antes que endpoints y vistas, y la página de React al
  final de su historia
- Las vistas de Inertia sólo arman props: ninguna lógica de negocio fuera de la capa de servicios

### Parallel Opportunities

- Fase 1: T003, T004, T005, T006, T007 y T011 en paralelo
- Fase 2: T013, T015, T016, T018, T024, T029, T031, T033, T036 y T041 en paralelo
- Fase 3: los cinco tests T042–T046 en paralelo; luego T048 junto a la implementación
- Fase 4: los cinco tests T060–T064 en paralelo; luego T065, T066, T067, T068 y T070 en paralelo
- Fase 5: T086, T087 y T088 en paralelo
- Fase 6: T098, T099, T100 y T101 en paralelo
- Fase 7: T110, T111 y T112 en paralelo
- Fase 9: T131, T132, T133 y T134 en paralelo
- Con varias personas: el trabajo de backend y el de las páginas de React de una misma historia
  pueden avanzar en paralelo una vez fijadas las props

---

## Parallel Example: User Story 1

```bash
# Los tests de US1, todos juntos:
Task: "Contrato de POST /credentials en tests/contract/test_credentials.py"
Task: "Contrato de POST /credentials/{id}/verify en tests/contract/test_credential_verify.py"
Task: "Contrato de GET /modules en tests/contract/test_modules.py"
Task: "Integración de carga y comprobación en tests/integration/test_credential_setup.py"
Task: "El secreto nunca sale, en tests/unit/test_secret_never_leaks.py"
```

---

## Implementation Strategy

### MVP (US1 + US2)

1. Fase 1 — Setup, incluida la cadena de assets
2. Fase 2 — Foundational, que bloquea todo lo demás
3. Fase 3 — US1: conectar Google guiado
4. Fase 4 — US2: ver la cobertura
5. **Parar y validar**: correr las secciones 4, 5 y 10 de quickstart.md contra un sitio propio
6. Desplegar y usarlo de verdad

### Entrega incremental

1. Setup + Foundational → base lista
2. US1 → validar → desplegar
3. US2 → validar → desplegar (**MVP funcional**)
4. US3 → validar → desplegar
5. US4 → validar → desplegar (**MVP en experiencia**: cualquiera puede usarlo solo)
6. US5 → validar → desplegar
7. US6 → validar → desplegar

### Qué queda deliberadamente afuera

Planes, cobro, verificación por registro TXT, acceso delegado por usuario final, módulos
adicionales de Google, varios proyectos por cuenta, Indexing API e IndexNow. Todo salvo las dos
últimas queda cableado y apagado por T013, T014 y T025. El detalle y el motivo están en el
apartado *Alcance postergado* de [spec.md](./spec.md).

---

## Notes

- `[P]` significa archivos distintos y sin dependencias pendientes
- Verificar que los tests fallan antes de implementar
- Confirmar antes de cada entrega: ninguna llamada a Google fuera del presupuesto, ningún estado
  de cobertura sin su fecha de obtención, ningún material secreto en ninguna superficie —incluidas
  las props que viajan al navegador— y ninguna capacidad apagada visible
