Adds a planned-phase doc for the next ask on top of the closed MVP2 cycle: weighted suggestion ordering + exclude items already on the active list. - Extends item_frequency (migration 026) with `weight integer` written via two admin-only SECURITY DEFINER RPCs (set_item_frequency_weight + purge_item_frequency). The client never writes weight directly — the existing all-deny policies on item_frequency stay in place. - fetchSuggestions reorders by `weight desc, use_count desc, last_used_at desc` and accepts an excludeNames arg to drop already-added items from the dropdown. - UI lives in /collective/manage as a 3-way segmented control per row (Hide / Normal / Boost mapping to -50 / 0 / 50). Admin writes; member read-only; guest hidden. Rebrand: MVP2 reopens from 6/6 ✅ to in-progress 6/7 ✅, with Fase 15 pending. Headers in fases 9–14 bump from "·N/6" + "fases 9 → 14" to "·N/7" + "fases 9 → 15". README status table + index updated, CLAUDE.md project status appends the Fase 15 pending paragraph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.5 KiB
Fase 12 — Section visibility (feature toggles user + colectivo)
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).
Objetivo: dar control granular sobre las cuatro secciones top-level actuales (Lists, Tasks, Notes, Search) sin construir un sistema de plugins. Cierra dos peticiones: usuarios que solo usan compra, y colectivos chiquitos que no necesitan tareas/notas.
Decisiones clave:
- Tres capas con precedencia: servidor (Fase 13) → colectivo (admin) → usuario. La capa más restrictiva gana. Si servidor dice off → off; si colectivo dice off → off para todos; si usuario dice off → off solo para él.
- Modelo de datos: JSONB
feature_flagsenusersy encollectives, con un schema típico-rígido (claves conocidas, no string libre). - Default: todo ON. Toggle off es opt-out.
- Regla del repo: toda sección top-level nueva requiere clave en
feature_flags+ checkbox en/settingsy en/collective/manage. Documentado enCLAUDE.md.
12.1 Modelo de datos
Fichero: supabase/migrations/022_section_visibility.sql.
alter table public.users
add column feature_flags jsonb not null default '{}'::jsonb;
alter table public.collectives
add column feature_flags jsonb not null default '{}'::jsonb;
-- Helper inmutable: secciones conocidas. Cambiar lista = nueva migración.
create or replace function public.known_sections()
returns text[] language sql immutable as $$
select array['lists','tasks','notes','search']
$$;
-- Helper: ¿está la sección activa para (user, collective)?
-- Precedencia: collective override > user override > default ON.
create or replace function public.section_enabled(
p_section text, p_user uuid, p_collective uuid
) returns boolean language sql stable as $$
select coalesce(
(select (feature_flags ->> p_section)::boolean
from public.collectives where id = p_collective),
(select (feature_flags ->> p_section)::boolean
from public.users where id = p_user),
true
)
$$;
Tareas:
- 12.1.1 Migración 022.
- 12.1.2 Tests pgTAP
supabase/tests/012_section_visibility.sql: default true; user off → false; collective off → false aunque user on; user on + collective on → true; sección desconocida ('foo') → true (default). - 12.1.3
just db-types.
12.2 Stores + tipos
Ficheros: apps/web/src/lib/stores/features.ts (nuevo), packages/types/src/domain.ts.
- 12.2.1 Tipo
SectionKey = 'lists' | 'tasks' | 'notes' | 'search'+FeatureFlags = Partial<Record<SectionKey, boolean>>. - 12.2.2 Store
enabledSectionsderivado decurrentUser.feature_flags+currentCollective.feature_flags, aplicando la misma precedencia quesection_enabledpara evitar viaje al servidor en cada render. - 12.2.3 Helpers
setUserFeature(key, value)ysetCollectiveFeature(key, value)que escriben a la columna JSONB (RPC dedicada oupdatedirecto con RLS — verificar coste de policy). - 12.2.4 Subscripción realtime a
users(propia fila) ycollectives(active) para reaccionar a cambios sin reload.
12.3 Gating de UI
Ficheros: apps/web/src/lib/components/layout/DesktopSidebar.svelte, MobileDrawer.svelte, BottomTabBar.svelte, todas las rutas (app)/lists, (app)/tasks, (app)/notes, (app)/search.
- 12.3.1 Filtrar entradas de navegación (sidebar + drawer + tab bar) por
$enabledSections. Sin animación al ocultar; cambio instantáneo. - 12.3.2 Guard en
+layout.ts(o+page.ts) de cada sección: si la sección está off, redirect a/con toast"$section_disabled_for_collective". - 12.3.3 Tab bar mobile recalcula columnas según número de secciones visibles (CSS grid
grid-template-columns: repeat(n, 1fr)). - 12.3.4 Si todas las secciones están off (edge case absurdo), forzar al menos "lists" siempre visible — único override hard en cliente.
12.4 UI de configuración
Ficheros: apps/web/src/routes/(app)/settings/+page.svelte, apps/web/src/routes/(app)/collective/manage/+page.svelte.
- 12.4.1
/settings→ nueva sección "Secciones visibles" con un toggle porSectionKey. Estado visual de "Override del colectivo" si el colectivo lo tiene off (toggle deshabilitado con tooltip). - 12.4.2
/collective/manage(solo admin) → bloque "Secciones del colectivo" con los mismos toggles. Cambios afectan a todos los miembros en tiempo real (realtime feed). - 12.4.3 Mensajes Paraglide:
settings_section_visibility_title,collective_section_visibility_title,section_disabled_for_collective, etiquetas por sección.
12.5 Convención y guía para el repo
- 12.5.1 Añadir bloque a
CLAUDE.mdbajo "Domain Model": "Toda sección top-level nueva requiere (a) entrada enpublic.known_sections(), (b) toggle por defecto en/settings, (c) toggle por defecto en/collective/manage, (d) test pgTAP que verifiquesection_enabled('newkey', ...)por defecto true." - 12.5.2 Plantilla de PR (si existe) o
docs/development.md→ checklist "nueva sección".
12.6 Tests
- 12.6.1 Vitest integración
tests/integration/section-visibility.test.ts: precedencia capa por capa; toggle de usuario no afecta a otro usuario; toggle de admin afecta a todos los miembros del colectivo. - 12.6.2 Playwright
tests/e2e/section-visibility.test.ts:- SV-01 Ana esconde "Tasks" en
/settings→ tab bar móvil y sidebar dejan de mostrarlo; navegar a/tasksredirige a/. - SV-02 Ana (admin) esconde "Notes" en
/collective/manage→ Borja (member, mismo colectivo) lo ve desaparecer sin reload. - SV-03 Borja apaga "Tasks" como usuario; cambia a otro colectivo donde sigue ON → "Tasks" vuelve a aparecer porque el override es por usuario, evaluado contra el colectivo activo.
- SV-01 Ana esconde "Tasks" en
12.Z Verificación + cierre
- 12.Z.1
just test-allverde. Suma esperada: +4 pgTAP + ~3 vitest + 3 playwright. - 12.Z.2 Auditoría manual: rotar las 4 secciones encendido/apagado en cada combinación user/collective, verificar que nada se rompe (rutas hijas como
/lists/[id]siguen accesibles si "lists" off por user-override pero on por colectivo — porque el deep link no debería romperse si el dueño tiene acceso al colectivo). - 12.Z.3 Documentar en
docs/history/fase-12-section-visibility.md.
Riesgos / notas
- Deep linking a sección oculta: decisión a tomar —
/lists/abccon "lists" off ¿muestra 404, redirige al home, o respeta el deep link? Propuesta MVP: redirigir al home con toast. Si molesta, abrir issue. - RLS en
users.feature_flags— escribir solo la propia fila; existe ya una policyusers_select_self/ similar, comprobar queupdateestá cubierto poruser_id = auth.uid(). - JSONB vs columnas explícitas: se elige JSONB para no requerir migración por cada sección nueva. Coste: tipado más débil en cliente; mitigado con el tipo
FeatureFlags = Partial<Record<SectionKey, boolean>>.
Scope explícito fuera
- Roles de feature (member vs guest vs admin con sets distintos). Solo dos planos: user + collective.
- Toggles a nivel de sub-feature dentro de una sección. Granularidad = sección top-level.
- UI para que un guest fuerce features de un colectivo. Guest sigue siendo read-only en config.