Stamps the six planned post-MVP fases as the MVP2 cycle (matching the `mvp2` branch name). Each plan file gets an "MVP2 · N/6" header. README status + index tables group them under an MVP2 bracket. CLAUDE.md project status reframes the planned line as MVP2 and clarifies that Fase 7+8 shipped as MVP cleanup, not MVP2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
125 lines
7.5 KiB
Markdown
125 lines
7.5 KiB
Markdown
### Fase 12 — Section visibility (feature toggles user + colectivo)
|
|
|
|
**MVP2 · 4/6.** Cuarta fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14).
|
|
|
|
**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_flags` en `users` y en `collectives`, 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 `/settings` y en `/collective/manage`. Documentado en `CLAUDE.md`.
|
|
|
|
---
|
|
|
|
#### 12.1 Modelo de datos
|
|
|
|
**Fichero:** `supabase/migrations/022_section_visibility.sql`.
|
|
|
|
```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 `enabledSections` derivado de `currentUser.feature_flags` + `currentCollective.feature_flags`, aplicando la misma precedencia que `section_enabled` para evitar viaje al servidor en cada render.
|
|
- [ ] **12.2.3** Helpers `setUserFeature(key, value)` y `setCollectiveFeature(key, value)` que escriben a la columna JSONB (RPC dedicada o `update` directo con RLS — verificar coste de policy).
|
|
- [ ] **12.2.4** Subscripción realtime a `users` (propia fila) y `collectives` (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 por `SectionKey`. 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.md` bajo "Domain Model": **"Toda sección top-level nueva requiere (a) entrada en `public.known_sections()`, (b) toggle por defecto en `/settings`, (c) toggle por defecto en `/collective/manage`, (d) test pgTAP que verifique `section_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 `/tasks` redirige 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.
|
|
|
|
---
|
|
|
|
#### 12.Z Verificación + cierre
|
|
|
|
- [ ] **12.Z.1** `just test-all` verde. 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/abc` con "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 policy `users_select_self` / similar, comprobar que `update` está cubierto por `user_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.
|