### Fase 4 — Búsqueda y Pulido Final
**Estado: 🟡 Búsqueda + RLS audit completos. PWA validation / push / Kong rate-limit / JWT rotation diferidos (requieren hardware real o config prod).**
**Duración estimada: 1 semana**  
**Objetivo: completar el MVP con calidad de producción.**

Esta fase sigue TDD: **primero los tests, al final la verificación.** Ninguna tarea de implementación se da por hecha hasta que todos los tests pasan (`just test-all`).

#### 4.0 Tests primero — escribir antes de implementar

**Vitest (`packages/test-utils/tests/`):**

- [x] `search.test.ts` — 10 tests: S-01 ítems / tareas / notas por texto; S-02 aislamiento cross-collective (Eva 0 filas, collective_id desconocido 0 filas); S-03 papelera excluida (RN-08); S-04 filtros por tipo, creador y rango de fechas ✅
- [x] `rls-audit.test.ts` — 42 tests: 3 intrusos (Ana/Borja/David) × 14 operaciones (SELECT/INSERT/UPDATE/DELETE sobre collectives, shopping_lists, shopping_items, task_lists, tasks, notes) → 0 filas ni mutaciones a través de colectivos ✅

**pgTAP (`supabase/tests/`):**

- [x] `007_search_tsvector.sql` — 9 tests: columnas `search` en shopping_items/tasks/notes, índices GIN creados, función `search_in_collective` existe, invariante funcional (vector generado refleja el texto insertado y matchea su `tsquery`) ✅
- [~] `008_rls_audit.sql` — sustituido por `rls-audit.test.ts` (Vitest) para evitar duplicar la matriz en SQL; la auditoría cross-collective se hace en TypeScript donde es trivial parametrizar por usuario+operación

**Playwright (`apps/web/tests/e2e/`):**

- [x] `search.test.ts` — SR-01 (buscar "milk" → grupo shopping_item con resultado), SR-02 (desactivar filtro "Listas" → grupo shopping_item desaparece) ✅
- [ ] `profile.test.ts` — diferido: `/profile` no implementado en este sprint (ver 4.2)
- [ ] `pwa.test.ts` — diferido a validación manual con `lighthouse-ci`
- [ ] `rate-limit.test.ts` — diferido a deploy de producción (Kong rate-limit plugin)

**Manual / herramientas externas:**

- [ ] Lighthouse PWA score ≥ 90 en mobile — pendiente (requiere build de producción)
- [ ] Instalable en iOS + Android — pendiente (requiere dispositivos reales)
- [ ] Notificaciones push iOS 16.4+ — diferido
- [x] Offline completo — cubierto por `O-01`/`O-02` en Playwright desde Fase 2b

#### 4.1 Búsqueda global

- [x] Migración `010_search_vectors.sql`:
  - Columnas STORED GENERATED `search tsvector` en `shopping_items`, `tasks`, `notes` (config `simple` — bilingüe en/es sin estemizar)
  - Índices GIN en cada una (`shopping_items_search_idx`, `tasks_search_idx`, `notes_search_idx`)
  - Enum `search_result_type` + función `search_in_collective(p_collective_id, p_query, p_types, p_creator, p_from, p_to)` con `SECURITY INVOKER` (RLS aplica automáticamente)
  - Trash excluido vía `shopping_lists.deleted_at IS NULL` y `notes.deleted_at IS NULL`
  - `GRANT EXECUTE` a `anon` + `authenticated`
- [x] Store `apps/web/src/lib/stores/search.ts` — wrapper RPC con tipado `SearchRow`
- [x] Pantalla `/search`:
  - Input con debounce de 300ms + filtrado mínimo de 2 caracteres
  - Filtros tipo (chips multi-toggle: Lists, Tasks, Notes — si todos activos no se envían, optimiza el SQL)
  - Resultados agrupados por tipo con `data-testid="search-group-<type>"` para Playwright
  - Deep-link según tipo (shopping_item → `/lists/[list_id]`, task → `/tasks/[list_id]`, note → `/notes/[id]`)
  - Filtros por creador y rango de fechas: soportados por la RPC; pendiente exponer en la UI cuando se necesite

#### 4.2 Perfil de usuario

- [ ] Pantalla `/profile` — **diferido**. La mayoría de la funcionalidad ya existe en `/settings` (nombre, avatar, idioma). Abandonar colectivo y eliminar cuenta no se han priorizado; los triggers SQL relevantes (promoción automática del admin más antiguo) ya están implementados y testeados desde Fase 1.

#### 4.3 PWA — Validación final

**Diferido a un sprint posterior** (requiere hardware y build de producción):

- [ ] `manifest.webmanifest` con iconos (512, 192, 180), theme-color, display: standalone
- [ ] Migrar Service Worker a `injectManifest` con `src/sw.ts` (ver gotcha #10 en CLAUDE.md)
- [ ] Notificaciones push con suscripción en el perfil

El PWA con `generateSW` de la Fase 2b cubre el caso MVP (precaching básico + offline para listas de compra).

#### 4.4 Seguridad y hardening

- [x] Auditoría de políticas RLS — `rls-audit.test.ts` (42 tests Vitest) prueba aislamiento cross-collective exhaustivo
- [ ] Rate limiting en Kong — diferido a deploy de producción
- [ ] Headers de seguridad en nginx — gestionado externamente en la config de nginx del VPS
- [ ] Rotación de `JWT_SECRET` y `ANON_KEY` para producción — diferido a deploy
- [ ] **Recordatorio:** al rotar `JWT_SECRET` hay que actualizar `root/.env` Y `apps/web/.env.development` a la vez, luego `docker compose up -d --force-recreate` + reiniciar Vite (ver `project_env_keys_trap` en la memoria)

#### 4.Z Verificación final — todos los tests verdes

- [x] `just test-all` → **211 verdes, 2 skipped** (34 pgTAP + 140 Vitest integration + 6 unit + 31 Playwright). Los 2 skipped siguen siendo los tests de presence bloqueados por bug upstream de `supabase/realtime`.
- [ ] Lighthouse PWA score ≥ 90 — manual, diferido
- [ ] Prueba de instalación PWA en iPhone + Android — manual, diferido
- [x] Aislamiento RLS auditado automáticamente — `rls-audit.test.ts` reemplaza la prueba manual con curl

**Criterio de aceptación:** búsqueda global por UI funcional, RLS auditado, `just test-all` verde. **Alcance de producción** (PWA install, push, rate-limit, JWT rotation) queda explícitamente marcado como diferido para un sprint de deploy.
