diff --git a/CLAUDE.md b/CLAUDE.md index 549f177..42d2800 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **Fase 0–5 complete. Fase 6 complete (deploy readiness: PWA injectManifest SW + manifest + iOS meta tags + placeholder icons + Kong rate-limit on invitations + dev JWT rotation + prod env template + rls-audit.sh + Justfile lighthouse/rls-audit/test-rate-limit). Production stack deployed to ambrosio (OVH VPS) at https://erosi.limonia.net (app + Supabase, external Keycloak); edge proxy now off-stack — internal Caddy fans kong/app on host port 3000, whatever external proxy fronts the host terminates TLS and forwards plain HTTP to ambrosio:3000 (originally NetBird's self-hosted Traefik, now proxy-agnostic); PWA strategy switched to generateSW to unblock the prod build (see the "injectManifest hardcoded filename" gotcha below). Fase 7 complete: 12 new Playwright tests fill the collective-flow UI gap (O-01..O-03 onboarding, I-01..I-05 invitation acceptance, MC-02..MC-05 admin-manage) — writing them surfaced + fixed three latent bugs in the product code (atomic `create_collective()` RPC, pendingInvitationToken restore after login, manage page not reloading on late $currentCollective hydration). Plus migration 013 (auth.users role/aud default trigger) — found after the first real self-registration on prod via Keycloak→GoTrue left the user with empty `role`/`aud` columns, breaking every REST call with `role "" does not exist`. Post-fase-7 prod auth hardening (2026-04-20..21, see `docs/history/fase-8-auth-logout.md`): migration 014 extends the role/aud guard to UPDATE (GoTrue's pop-ORM resends role='' right after the INSERT and clobbered the INSERT trigger's backfill); `logout()` now does RP-initiated Keycloak end-session + lands on a new public `/logged-out` route, which both actually ends the Keycloak SSO cookie (so the next login re-prompts) and closes a PKCE-verifier race that produced "PKCE code verifier not found in storage" on prod. 259 tests green: 45 pgTAP + 140 Vitest integration + 15 Vitest unit + 59 Playwright + 1 Vitest rate-limit (gated, run via `just test-rate-limit`). 3 skipped in `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). Push notifications + CI Docker + final production icons deliberately out of scope. ✅** -**MVP2 — COMPLETE (6/6 ✅, 2026-05-18).** Branch `mvp2`. **Fase 9: dark mode + polish** (migrations 015–016 + pgTAP 009–010 + dark-mode plumbing + max-w-2xl reading column + sync_conflicts table/queue logging + WebKit Playwright project; see `docs/history/fase-9-polish-dark-mode.md`). **Fase 10: spec completion (H06/H07/H08 + trash restore + account deletion + sidebar-create + reset-from-detail + Accept-Language)** — migrations 017–021 + 4 new pgTAP suites + 6 new Playwright specs; see `docs/history/fase-10-spec-completion.md`. **Fase 11: item tags + drag-reorder** — migration 022 + pgTAP 014 + tags store + chips/picker + filter bar + realtime re-sort; see `docs/history/fase-11-item-tags-importance.md`. **Fase 12: section visibility (feature flags)** — migration 023 + pgTAP 015 + JSONB `feature_flags` on `users`+`collectives` + `public.section_enabled()` SQL helper with COALESCE precedence; see `docs/history/fase-12-section-visibility.md`. **Fase 13: server administration** — migrations 024+025 (server_admins + admin_actions + server_settings + 9 SECURITY DEFINER admin RPCs) + pgTAP 016+017 + `/admin` route group + last-admin guard + audit-append-only via missing-policies pattern + `section_enabled()` extended with server layer + `SERVER_ADMIN_EMAIL` bootstrap (prod needs manual re-run after the operator's first Keycloak login); see `docs/history/fase-13-server-admin.md`. **Fase 14: PWA hardening** — `useRegisterSW` + `UpdateToast` (auto-update toast) + offline chip + queued-op badge + `/settings/sync-conflicts` review route + `__APP_VERSION__`/`__APP_COMMIT__`/`__BUILD_TIME__` defines baked at build via Dockerfile `GIT_SHA` build-arg (host-side `git rev-parse --short HEAD` in `deploy-erosi.sh`) + "Acerca de" version chip in `/settings`; no migration; see `docs/history/fase-14-pwa-hardening.md`. **Final test totals (`just test-all`): 166 pgTAP + 173 Vitest integration + 47 Vitest unit + 89 Playwright + 1 gated rate-limit = 476 tests green; 3 skipped (2 Realtime presence — upstream `handle_out/3` bug `supabase/realtime#1617`; 1 rate-limit — gated). Delta vs MVP close: +121 pgTAP + +33 integration + +32 unit + +30 e2e = +217 tests across MVP2.** Fase 7+8 already shipped as MVP cleanup, not MVP2. +**MVP2 — in progress (6/7 ✅).** Branch `mvp2`. Original cycle (fases 9–14) shipped 2026-05-18; ciclo reabierto con **Fase 15** añadida después. **Fase 9: dark mode + polish** (migrations 015–016 + pgTAP 009–010 + dark-mode plumbing + max-w-2xl reading column + sync_conflicts table/queue logging + WebKit Playwright project; see `docs/history/fase-9-polish-dark-mode.md`). **Fase 10: spec completion (H06/H07/H08 + trash restore + account deletion + sidebar-create + reset-from-detail + Accept-Language)** — migrations 017–021 + 4 new pgTAP suites + 6 new Playwright specs; see `docs/history/fase-10-spec-completion.md`. **Fase 11: item tags + drag-reorder** — migration 022 + pgTAP 014 + tags store + chips/picker + filter bar + realtime re-sort; see `docs/history/fase-11-item-tags-importance.md`. **Fase 12: section visibility (feature flags)** — migration 023 + pgTAP 015 + JSONB `feature_flags` on `users`+`collectives` + `public.section_enabled()` SQL helper with COALESCE precedence; see `docs/history/fase-12-section-visibility.md`. **Fase 13: server administration** — migrations 024+025 (server_admins + admin_actions + server_settings + 9 SECURITY DEFINER admin RPCs) + pgTAP 016+017 + `/admin` route group + last-admin guard + audit-append-only via missing-policies pattern + `section_enabled()` extended with server layer + `SERVER_ADMIN_EMAIL` bootstrap (prod needs manual re-run after the operator's first Keycloak login); see `docs/history/fase-13-server-admin.md`. **Fase 14: PWA hardening** — `useRegisterSW` + `UpdateToast` (auto-update toast) + offline chip + queued-op badge + `/settings/sync-conflicts` review route + `__APP_VERSION__`/`__APP_COMMIT__`/`__BUILD_TIME__` defines baked at build via Dockerfile `GIT_SHA` build-arg (host-side `git rev-parse --short HEAD` in `deploy-erosi.sh`) + "Acerca de" version chip in `/settings`; no migration; see `docs/history/fase-14-pwa-hardening.md`. **Final test totals (`just test-all`): 166 pgTAP + 173 Vitest integration + 47 Vitest unit + 89 Playwright + 1 gated rate-limit = 476 tests green; 3 skipped (2 Realtime presence — upstream `handle_out/3` bug `supabase/realtime#1617`; 1 rate-limit — gated). Delta vs MVP close: +121 pgTAP + +33 integration + +32 unit + +30 e2e = +217 tests across MVP2 fases 9–14.** Fase 7+8 already shipped as MVP cleanup, not MVP2. + +**Pending in MVP2 (📋): Fase 15 — Common items management** (`plan/fase-15-common-items.md`). Extends `item_frequency` (migration 026) with a `weight integer` column written via admin-only SECURITY DEFINER RPCs (`set_item_frequency_weight` + `purge_item_frequency`). Suggestion query reorders by `weight desc, use_count desc, last_used_at desc`. UI 3-way segmented control (Hide / Normal / Boost → -50 / 0 / 50) lives in `/collective/manage` (admin write, member read-only, guest hidden). `fetchSuggestions` gains `excludeNames` arg to drop items already on the active list from the dropdown. ## Documentation Map diff --git a/README.md b/README.md index 8c408d1..d77b427 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,14 @@ | Fase 6 — Deploy readiness | ✅ Completa (PWA generateSW + manifest + iconos placeholder + Kong rate-limit en invitaciones + rotación JWT dev + rotate-jwt.sh + `.env.erosi.example` + rls-audit.sh + `just lighthouse`). Rate-limit auth retirado — Keycloak brute-force nativa cubre el caso. Push notifications y iconos finales fuera de scope. | | Fase 7 — Collective-flow E2E tests | ✅ Completa (12 Playwright nuevos O-01..MC-05; 3 bugs latentes corregidos: RPC atómica `create_collective`, restore de `pendingInvitationToken` tras login, hidratación tardía de `$currentCollective` en manage; migración 013 trigger `role/aud` default). | | Fase 8 — Prod auth hardening | ✅ Completa (migración 014 extiende `role/aud` guard a UPDATE; `logout()` RP-initiated Keycloak end-session + ruta pública `/logged-out`). | -| **MVP2** — Fase 9 → 14 | ✅ Completa (rama `mvp2`, 2026-05-18) | +| **MVP2** — Fase 9 → 15 | 🔄 6/7 (fases 9–14 ✅ 2026-05-18; Fase 15 📋) | | ↳ Fase 9 — Polish + Dark Mode | ✅ | | ↳ Fase 10 — Spec completion | ✅ | | ↳ Fase 11 — Item tags + importance UX | ✅ | | ↳ Fase 12 — Section visibility | ✅ | | ↳ Fase 13 — Server administration | ✅ | | ↳ Fase 14 — PWA hardening | ✅ | +| ↳ Fase 15 — Common items management | 📋 | | Suite de tests (pgTAP + Vitest + Playwright) | ✅ **476 tests verdes** (166 pgTAP + 173 integración + 47 unit + 89 E2E) + 1 rate-limit gated (`just test-rate-limit`). 3 skipped en `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). | --- @@ -59,6 +60,7 @@ Las fases ya cerradas (0, 1, 2a) reflejan este patrón retroactivamente: la secc | **MVP2 — Fase 12** | [fase-12-section-visibility.md](plan/fase-12-section-visibility.md) | 1 semana | | **MVP2 — Fase 13** | [fase-13-server-admin.md](plan/fase-13-server-admin.md) | 2–3 semanas | | **MVP2 — Fase 14** | [fase-14-pwa-hardening.md](plan/fase-14-pwa-hardening.md) | 3–5 días | +| **MVP2 — Fase 15** | [fase-15-common-items.md](plan/fase-15-common-items.md) | 3–5 días | **Total estimado MVP (Fase 0–6): 11–16 semanas. MVP2 (Fase 9–14, rama `mvp2`): 10–14 semanas. Fase 7+8 entre MVP y MVP2 como cleanup post-launch.** diff --git a/plan/fase-10-spec-completion.md b/plan/fase-10-spec-completion.md index 22f46d4..6a3185d 100644 --- a/plan/fase-10-spec-completion.md +++ b/plan/fase-10-spec-completion.md @@ -1,6 +1,6 @@ ### Fase 10 — Spec completion ("cerrar la laguna del `analisis-funcional`") -**MVP2 · 2/6.** Segunda fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14). +**MVP2 · 2/7.** Segunda fase del ciclo MVP2 (rama `mvp2`, fases 9 → 15). **Estado: ✅ Completa (2026-05-18). Cierra las 8 piezas de "completion debt" del audit 2026-04-22. Migraciones 017–021, pgTAP 010–013, 6 specs E2E + 2 integración + 12 unit. Ver `docs/history/fase-10-spec-completion.md`.** diff --git a/plan/fase-11-item-tags-importance.md b/plan/fase-11-item-tags-importance.md index 60129d0..82a6f5b 100644 --- a/plan/fase-11-item-tags-importance.md +++ b/plan/fase-11-item-tags-importance.md @@ -1,6 +1,6 @@ ### Fase 11 — Item tags + importance UX -**MVP2 · 3/6.** Tercera fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14). +**MVP2 · 3/7.** Tercera fase del ciclo MVP2 (rama `mvp2`, fases 9 → 15). **Estado: 📋 Planificada. Cierra dos huecos en la experiencia del item de compra: clasificación libre por tags (scope colectivo) y reordenación explícita por arrastrar (importance).** diff --git a/plan/fase-12-section-visibility.md b/plan/fase-12-section-visibility.md index 3d08544..16b910c 100644 --- a/plan/fase-12-section-visibility.md +++ b/plan/fase-12-section-visibility.md @@ -1,6 +1,6 @@ ### Fase 12 — Section visibility (feature toggles user + colectivo) -**MVP2 · 4/6.** Cuarta fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14). +**MVP2 · 4/7.** Cuarta fase del ciclo MVP2 (rama `mvp2`, fases 9 → 15). **Estado: 📋 Planificada. Permite a cada usuario ocultar secciones top-level que no usa, y a admins de colectivo ocultarlas para todo el colectivo. Pre-requisito para Fase 13 (server admin reusa el mismo modelo a nivel servidor).** diff --git a/plan/fase-13-server-admin.md b/plan/fase-13-server-admin.md index 3be5af0..7778e26 100644 --- a/plan/fase-13-server-admin.md +++ b/plan/fase-13-server-admin.md @@ -1,6 +1,6 @@ ### Fase 13 — Server administration -**MVP2 · 5/6.** Quinta fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14). +**MVP2 · 5/7.** Quinta fase del ciclo MVP2 (rama `mvp2`, fases 9 → 15). **Estado: 📋 Planificada. Mayor salto de scope hasta ahora — introduce un rol global ortogonal a los roles de colectivo, un área `/admin` nueva, RPCs con privilegios elevados, y un audit log. Requiere Fase 12 para los toggles de visibilidad a nivel servidor.** diff --git a/plan/fase-14-pwa-hardening.md b/plan/fase-14-pwa-hardening.md index c4d10d0..b7dbef5 100644 --- a/plan/fase-14-pwa-hardening.md +++ b/plan/fase-14-pwa-hardening.md @@ -1,6 +1,6 @@ ### Fase 14 — PWA hardening (auto-update + offline polish + version chip) -**MVP2 · 6/6.** Última fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14). +**MVP2 · 6/7.** Última fase del ciclo MVP2 (rama `mvp2`, fases 9 → 15). **Estado: 📋 Planificada. Pulido del PWA que Fase 6 dejó funcional pero crudo. Sin migraciones de DB. Depende de Fase 9.3 (wiring de `sync_conflicts`) para el badge de conflictos.** diff --git a/plan/fase-15-common-items.md b/plan/fase-15-common-items.md new file mode 100644 index 0000000..ba2c46e --- /dev/null +++ b/plan/fase-15-common-items.md @@ -0,0 +1,187 @@ +### Fase 15 — Common items management (weighted suggestions + exclude-on-list) + +**MVP2 · 7/7.** Añadida después del cierre de MVP2 (2026-05-18) para integrar dos peticiones concretas sobre items frecuentes; reabre el ciclo con una fase extra. + +**Estado: 📋 Planificada.** Atiende dos peticiones concretas sobre el flujo "añadir item a una lista": + +1. **Promover items** — algunos items aparecen siempre primero en las sugerencias (independientemente de `use_count`). +2. **Ocultar items ya añadidos** — la lista activa no debe sugerir items que ya están en ella. + +**Objetivo: dar a admin (o member, decidir) un sitio para curar el catálogo de items frecuentes del colectivo — promover los recurrentes, retirar ruido — y limpiar la UX de "ver sugerencias duplicadas de lo que ya tengo en la lista". Sin construir un sistema de plantillas — sigue siendo `item_frequency` con un dial extra.** + +**Estado actual (auditado 2026-05-18):** +- `supabase/migrations/006_item_frequency.sql` — tabla `item_frequency (collective_id, name, use_count, last_used_at)`, escrita por trigger SECURITY DEFINER sobre `shopping_items` INSERT. Todos los INSERT/UPDATE/DELETE del cliente están bloqueados por policy. +- `apps/web/src/lib/stores/lists.ts:275-300` (`fetchSuggestions`) — ordena por `use_count DESC`, top 5 sin prefijo / top 10 con prefijo. +- `apps/web/src/lib/components/ItemSuggestions.svelte` — render del dropdown. +- Sin UI de gestión. + +**Decisiones clave:** +- Extender `item_frequency` con `weight integer not null default 0`. UI 3-way (Hide / Normal / Boost) mapea a `-50 / 0 / 50`; el rango DB acepta cualquier integer (sin check constraint) por simplicidad y forward-compat. Sigue siendo una sola tabla; nada de catálogo separado. +- Escritura de `weight` vía SECURITY DEFINER RPC — el bloqueo all-deny de la migración 006 se mantiene; solo la RPC nueva puede modificar `weight`. +- Borrar fila de `item_frequency` por completo (purgar) vía otra RPC SECURITY DEFINER — útil para limpiar typos/ruido. No toca `shopping_items`. +- Permiso para gestionar: **admin del colectivo** (consistente con CU-H07 rename). Members ven la lista en read-only; guests ni eso. +- Suggestion query: `order by weight desc, use_count desc, last_used_at desc`. Empate por `weight` cae al `use_count` actual. +- Exclude-on-list: `fetchSuggestions` recibe `excludeNames: string[]` opcional. Se aplica `.not('name', 'in', (...))` en el query. Lista vacía = sin filtro (cero coste). +- Sin migración de datos para items existentes — todos arrancan con `weight=0`, el orden cambia solo cuando un admin promueve algo. + +--- + +#### 15.1 Modelo + RPCs + +**Fichero:** `supabase/migrations/026_item_frequency_weight.sql`. + +```sql +alter table public.item_frequency + add column weight integer not null default 0; + +-- Re-create the suggestion index to include weight first. +drop index if exists item_frequency_collective_count_idx; +create index item_frequency_collective_order_idx + on public.item_frequency (collective_id, weight desc, use_count desc, last_used_at desc); + +-- Admin-only write path for weight. +create or replace function public.set_item_frequency_weight( + p_collective_id uuid, + p_name text, + p_weight integer +) returns void +language plpgsql +security definer +set search_path = public, auth +as $$ +declare + v_role text; + v_norm text := lower(trim(p_name)); +begin + if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if; + select role into v_role from public.collective_members + where collective_id = p_collective_id and user_id = auth.uid(); + if v_role is distinct from 'admin' then + raise exception 'only admins can manage common items' using errcode = 'P0002'; + end if; + insert into public.item_frequency (collective_id, name, use_count, weight, last_used_at) + values (p_collective_id, v_norm, 0, p_weight, now()) + on conflict (collective_id, name) + do update set weight = excluded.weight; +end; $$; + +grant execute on function public.set_item_frequency_weight(uuid, text, integer) + to authenticated; + +-- Admin-only purge. +create or replace function public.purge_item_frequency( + p_collective_id uuid, p_name text +) returns void +language plpgsql +security definer +set search_path = public, auth +as $$ +declare + v_role text; +begin + if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if; + select role into v_role from public.collective_members + where collective_id = p_collective_id and user_id = auth.uid(); + if v_role is distinct from 'admin' then + raise exception 'only admins can purge common items' using errcode = 'P0002'; + end if; + delete from public.item_frequency + where collective_id = p_collective_id and name = lower(trim(p_name)); +end; $$; + +grant execute on function public.purge_item_frequency(uuid, text) to authenticated; +``` + +**Tareas:** + +- [ ] **15.1.1** Migración 026. +- [ ] **15.1.2** Tests pgTAP `supabase/tests/018_item_frequency_weight.sql`: + - default 0 al crear desde el trigger existente. + - `set_item_frequency_weight` como admin → upsert OK. + - `set_item_frequency_weight` como member → P0002. + - `purge_item_frequency` como admin borra fila. + - `purge_item_frequency` como guest → P0002. + - `set_item_frequency_weight` sobre un nombre nuevo (que no existe en `item_frequency`) inserta fila con use_count=0. +- [ ] **15.1.3** `just db-types` para regenerar `packages/types/src/database.ts`. + +--- + +#### 15.2 Store + query + +**Fichero:** `apps/web/src/lib/stores/lists.ts` (extender `fetchSuggestions`), nuevo `apps/web/src/lib/stores/commonItems.ts`. + +- [ ] **15.2.1** Extender `fetchSuggestions(collectiveId, prefix, options?)` con `options = { excludeNames?: string[]; limit?: number }`. Aplicar `.not('name', 'in', '(...)')` si `excludeNames` tiene >0 entries. Cambiar `.order('use_count')` por `.order('weight', { ascending: false }).order('use_count', { ascending: false })`. +- [ ] **15.2.2** En el callsite de `/lists/[id]/+page.svelte`, pasar `excludeNames = items.map(i => i.name.toLowerCase().trim())`. Recompute on items mutation (Svelte 5 `$derived`). +- [ ] **15.2.3** `commonItems.ts` store: + - `loadCommonItems(collectiveId)` — `select *` from `item_frequency` ordered por `weight desc, use_count desc`, sin limit. + - `setWeight(collectiveId, name, weight)` — invoca RPC `set_item_frequency_weight`. + - `purge(collectiveId, name)` — invoca RPC `purge_item_frequency`. +- [ ] **15.2.4** Tests integración `packages/test-utils/tests/common-items.test.ts`: + - CI-INT-01 admin set weight → la siguiente `fetchSuggestions` lo devuelve primero. + - CI-INT-02 `excludeNames` filtra correctamente. + - CI-INT-03 member intenta set weight → falla con P0002. + +--- + +#### 15.3 UI de gestión + +**Fichero:** `apps/web/src/routes/(app)/collective/manage/+page.svelte` (admin) — nueva subsección "Items frecuentes". Si la sección crece mucho, sacarla a `apps/web/src/routes/(app)/collective/manage/common-items/+page.svelte` (decidir según diseño). + +**Decisión de ubicación:** `/collective/manage` y no `/settings`, porque es config de colectivo (cross-user). `/settings` queda para preferencias del propio usuario. + +- [ ] **15.3.1** Tabla con columnas: `name | weight | use_count | last_used_at | acciones`. Sort default: `weight DESC, use_count DESC`. Search box (filtro client-side por substring). +- [ ] **15.3.2** Acciones por fila: + - Selector 3-way **Hide / Normal / Boost** — segmented control de 3 botones que escriben `weight = -50 / 0 / 50` respectivamente. Estado actual destacado visualmente. Sin números expuestos al usuario. + - Botón "Purgar" (icono trash) con modal de confirmación "Esto solo borra la entrada de sugerencias; los items existentes en listas no se tocan". +- [ ] **15.3.3** Botón "Añadir item común" — abre modal con input + el mismo selector 3-way (default `Boost`, weight=50). Llama a `setWeight(collective, name, 50)`. Util para sembrar el catálogo antes de que las listas reales generen `use_count`. +- [ ] **15.3.4** Si el colectivo no tiene un solo `item_frequency` (caso colectivo nuevo), mostrar empty state explicativo. +- [ ] **15.3.5** Members ven la tabla pero todas las acciones están deshabilitadas con tooltip "Solo admins pueden gestionar items comunes". Guests no ven la sección. + +--- + +#### 15.4 Exclude-on-list en la sugerencia + +**Fichero:** `apps/web/src/routes/(app)/lists/[id]/+page.svelte` + `apps/web/src/lib/components/ItemSuggestions.svelte`. + +- [ ] **15.4.1** Pasar `excludeNames` a `fetchSuggestions` en el callsite de añadir item. +- [ ] **15.4.2** Cuando el usuario añade un item desde el dropdown, refrescar el set de exclusión inmediatamente (el siguiente teclazo en el input ya no debería ver ese item). Reactivo vía `$derived`. +- [ ] **15.4.3** Si tras filtrar `excludeNames` y aplicar `weight` el resultado es vacío, no mostrar el dropdown (no fallback a use_count puro — la idea es que la lista está completa). + +--- + +#### 15.5 Tests + +- [ ] **15.5.1** Vitest unit `tag-picker-filter.test.ts`-style para el ordenador local — si se decide hacer ordering client-side adicional (probablemente no — el server hace `order by` correcto). +- [ ] **15.5.2** Playwright `tests/e2e/common-items.test.ts`: + - **CI-01** Ana (admin) clickea `Boost` sobre "leche" en `/collective/manage` (escribe weight=50) → en `/lists/` el dropdown de sugerencias muestra "leche" primero, aunque su use_count sea menor que otros items. + - **CI-02** Ana añade "pan" a la lista activa → al volver al input, las sugerencias dejan de incluir "pan". + - **CI-03** Borja (member) entra a `/collective/manage` → ve la tabla, los steppers están disabled. + - **CI-04** Ana purga "agua" → la fila desaparece y el item ya no aparece en sugerencias futuras (hasta que alguien la vuelva a añadir y el trigger la recree con weight=0). + +--- + +#### 15.Z Verificación + cierre + +- [ ] **15.Z.1** `just test-all` verde. Suma esperada: +7 pgTAP + 3 integración + 4 playwright = +14. +- [ ] **15.Z.2** Auditoría manual: crear un colectivo nuevo, añadir 5 listas con items repetidos, promover 2, purgar 1, verificar orden en sugerencias. +- [ ] **15.Z.3** Lighthouse contra `/collective/manage` con 100 items frecuentes (semilla) — no regresión. +- [ ] **15.Z.4** Documentar en `docs/history/fase-15-common-items.md`. +- [ ] **15.Z.5** Actualizar `messages/en.json` + `messages/es.json` con los strings nuevos. + +--- + +### Riesgos / notas + +- **Mapeo Hide/Normal/Boost → -50/0/50**: confirmado por el usuario en el momento de planificar. La columna `weight integer` no tiene check constraint para no atarse a estos 3 valores si más adelante hace falta granularidad (p.ej. distinguir "Boost fuerte" de "Boost suave"). El cliente solo escribe los 3 valores canónicos; cualquier otro valor en la DB es histórico/manual. +- **`item_frequency` no tiene columna `created_by`** — la fila la inserta un trigger SECURITY DEFINER. Si se quiere audit "quién promovió qué", añadir tabla `item_frequency_audit` aparte. Fuera de scope MVP. +- **Realtime no está activado sobre `item_frequency`** (verificar `007_realtime_publication.sql`). Si dos admins editan a la vez no se ven en vivo. Aceptable; admin UI hace polling al recargar la página. +- **Conflicto con tags (Fase 11)**: los tags son `m:n` sobre `shopping_items`, no `item_frequency`. Un item promovido sin tags no asocia tags por defecto al añadirse. Decisión: dejarlo así — promover y etiquetar son ejes ortogonales. +- **Coste del `not in (...)` con listas largas**: si una lista tiene 200 items, la query `excludeNames` pasa un array de 200 strings. PostgREST construye un `NOT IN ('a','b',...)` literal — coste O(n*m). Para listas <100 items es despreciable; para >>100 reconsiderar a un `LEFT JOIN ... WHERE shopping_items.id IS NULL` vía RPC. + +### Scope explícito fuera + +- Importar/exportar items comunes entre colectivos. +- Sugerencias de categorías (frutería / lácteos / …). +- Items comunes globales del servidor (cross-collective). Solo per-colectivo. +- Recomendaciones basadas en ML/historial cruzado. +- Tags pre-asignados a items promovidos. diff --git a/plan/fase-9-polish-dark-mode.md b/plan/fase-9-polish-dark-mode.md index f7d11ef..6c06df4 100644 --- a/plan/fase-9-polish-dark-mode.md +++ b/plan/fase-9-polish-dark-mode.md @@ -1,6 +1,6 @@ ### Fase 9 — Polish + Dark Mode -**MVP2 · 1/6.** Primera fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14). +**MVP2 · 1/7.** Primera fase del ciclo MVP2 (rama `mvp2`, fases 9 → 15). **Estado: ✅ Completada (2026-05-18). Ver `docs/history/fase-9-polish-dark-mode.md` para detalles + paleta definitiva. 41 tests nuevos: 8 user_theme pgTAP, 12 sync_conflicts pgTAP, 4 sync-conflicts integración, 3 SC unit, 8 theme unit, 3 theme E2E, 3 reading-width E2E. WebKit specs (2 nuevos) gated en `RUN_WEBKIT=1`.**