Files
collective-lists/plan/fase-12-section-visibility.md
Oier Bravo Urtasun 6e0814169d docs(plan): fases 11–14 — tags, section visibility, server admin, PWA hardening
Adds four planned-phase docs covering the next post-MVP roadmap:

- Fase 11 — item tags (collective-scoped) + drag-reorder UI for the
  existing `shopping_items.sort_order` column. Migration 021.
- Fase 12 — section visibility via JSONB `feature_flags` on `users` and
  `collectives`, with a `section_enabled()` SQL helper that resolves
  precedence (collective > user > default ON). Migration 022.
- Fase 13 — `/admin` area with a global `server_admins` role, append-only
  `admin_actions` audit log, RPCs for collective CRUD + member removal,
  and server-level section-visibility defaults that sit above Fase 12.
  Migrations 023+024. Depends on Fase 12.
- Fase 14 — PWA hardening: `onNeedRefresh` update toast, explicit offline
  indicator + sync queue badge + conflict surface, `__APP_VERSION__` and
  `__APP_COMMIT__` baked at build with a chip in `/settings`. No migration.
  Depends on Fase 9.3 (`sync_conflicts` wiring).

Also bumps CLAUDE.md project status with the planned-fases pointer, and
brings README.md status + index tables in line with Fase 7, 8, 9, 10
(previously missing) plus the four new ones.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:38:23 +02:00

7.5 KiB

Fase 12 — Section visibility (feature toggles user + colectivo)

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.

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.