Files
collective-lists/plan/fase-14-pwa-hardening.md
Oier Bravo Urtasun 93153cd750 docs(plan): brand fases 9–14 as MVP2
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>
2026-05-18 00:42:08 +02:00

7.2 KiB

Fase 14 — PWA hardening (auto-update + offline polish + version chip)

MVP2 · 6/6. Última fase del ciclo MVP2 (rama mvp2, fases 9 → 14).

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.

Objetivo: convertir el PWA de "técnicamente instalable" a "se actualiza solo, avisa cuando estás offline, y muestra qué versión corres". Tres asks pequeños, una fase pequeña.

Estado actual (auditado el 2026-05-18):

  • apps/web/src/sw.ts ya llama self.skipWaiting() y clientsClaim() — el SW nuevo toma control en cuanto entra (no requiere cerrar todas las pestañas).
  • apps/web/src/routes/+layout.svelte:15 registra con registerSW({ immediate: true }) sin callbacks. No hay UX para avisar al usuario de update.
  • Estrategia generateSW activa (ver gotcha "injectManifest hardcoded filename" en CLAUDE.md). Workbox precachea assets.
  • IndexedDB offline-first existe (CLAUDE.md "Sync Strategy"). sync_conflicts se llena pero no se enseña.

14.1 Auto-update con toast

Ficheros: apps/web/src/routes/+layout.svelte, nuevo apps/web/src/lib/components/UpdateToast.svelte.

  • 14.1.1 Migrar de registerSW({ immediate: true }) a useRegisterSW({ onNeedRefresh, onOfflineReady }) desde virtual:pwa-register/svelte.
  • 14.1.2 UpdateToast.svelte — pill flotante bottom-center: "Nueva versión disponible — toca para actualizar" + botón "Recargar". Click invoca updateSW(true) (force reload).
  • 14.1.3 Cuando onOfflineReady dispara la primera vez (instalación inicial), toast distinto: "Lista para usar offline". Auto-dismiss a 4s.
  • 14.1.4 Telemetría mínima (console.info con prefijo [pwa]): registro, update detectado, update aplicado. Sin externalizar — solo facilitar debug de campo.
  • 14.1.5 Test Playwright tests/e2e/pwa-update.test.ts PU-01: deploy mock con nuevo SW → toast aparece → click recarga → app sirve nuevo bundle. Stubs vía addInitScript para simular onNeedRefresh.

14.2 Offline polish

Ficheros: apps/web/src/lib/components/SyncBanner.svelte (existe), apps/web/src/lib/stores/network.ts (verificar; crear si no existe), apps/web/src/routes/(app)/+layout.svelte.

Auditoría previa: SyncBanner ya existe. Verificar si maneja navigator.onLine + listener online/offline. Reusar lo que haya.

  • 14.2.1 Indicador explícito de estado offline: chip pequeño persistente en MobileTopBar y DesktopSidebar cuando !navigator.onLine. Color bg-amber-500/10 text-amber-700. Tooltip: "Sin conexión — los cambios se sincronizarán al volver".
  • 14.2.2 Badge con número de operaciones encoladas. Lee del store de sync existente (verificar nombre en apps/web/src/lib/stores/). 0 → sin badge; >0 → número junto al chip offline.
  • 14.2.3 Surface de conflictos (depende de Fase 9.3): si hay filas en sync_conflicts para el usuario, banner amarillo en (app)/+layout.svelte con link a /settings/sync-conflicts (nueva ruta — tabla de conflictos con opción "descartar local" / "descartar remoto").
  • 14.2.4 Fallback de Background Sync API en Safari (gotcha en CLAUDE.md): listener window.addEventListener('online', flushQueue). Verificar que está implementado; si no, añadir.
  • 14.2.5 Tests Playwright tests/e2e/offline.test.ts:
    • OF-01 context.setOffline(true), añadir item → chip offline visible + badge "1" → setOffline(false) → badge desaparece + item sincronizado en otro contexto.
    • OF-02 Forzar conflicto (escribir local + escribir remoto antes de flush) → banner conflicto aparece → /settings/sync-conflicts lista la fila → "descartar local" la elimina.

14.3 Server version en perfil

Ficheros: apps/web/vite.config.ts, apps/web/src/routes/(app)/settings/+page.svelte, apps/web/src/lib/version.ts (nuevo).

  • 14.3.1 En vite.config.ts, inyectar define: { __APP_VERSION__: JSON.stringify(<value>), __APP_COMMIT__: JSON.stringify(<value>) }:
    • __APP_VERSION__ = package.json version del root.
    • __APP_COMMIT__ = process.env.GIT_SHA || execSync('git rev-parse --short HEAD') con fallback a 'dev' si no hay git.
  • 14.3.2 apps/web/src/lib/version.ts exporta { version: __APP_VERSION__, commit: __APP_COMMIT__, builtAt: __BUILD_TIME__ }. Añadir __BUILD_TIME__ = new Date().toISOString() al define.
  • 14.3.3 Declarar los tres símbolos en apps/web/src/global.d.ts.
  • 14.3.4 Sección "Acerca de" en /settings/+page.svelte al final: chip pequeño text-xs text-muted con v{version} · {commit} · {builtAt.slice(0,10)}. No clicable, solo info.
  • 14.3.5 Bake durante el build de Docker (apps/web/Dockerfile): pasar GIT_SHA como build arg, y desde infra/scripts/deploy-erosi.sh capturar git rev-parse --short HEAD localmente y reenviar como --build-arg GIT_SHA=$(git rev-parse --short HEAD).
  • 14.3.6 Test unit apps/web/src/lib/version.test.ts: en entorno de test (Vitest sin define) usa stubs vía vi.stubGlobal; verifica que version no sea undefined ni 'dev' cuando el define está activo.

14.Z Verificación + cierre

  • 14.Z.1 just test-all verde. Suma esperada: +1 vitest + 3 playwright.
  • 14.Z.2 Smoke manual del flujo update: build, deploy, instalar PWA en móvil, redeploy con versión bumpeada, esperar el toast (≤ 60s con updateViaCache: 'none'), aplicar, verificar versión nueva en /settings.
  • 14.Z.3 Lighthouse PWA score en https://erosi.limonia.net: objetivo ≥ 90 (installable + offline + correct manifest).
  • 14.Z.4 Documentar en docs/history/fase-14-pwa-hardening.md.

Riesgos / notas

  • skipWaiting agresivo puede romper sesiones activas con cambios optimistas en vuelo. Mitigación: el toast pide confirmación del usuario antes de updateSW(true). Si el usuario ignora, el SW nuevo entra de todos modos en la siguiente navegación — aceptable.
  • onNeedRefresh no se dispara si Workbox no detecta cambios en el manifest. El build debe alterar al menos un asset hasheado o el revision del precache no cambia. generateSW lo hace por defecto, pero un deploy que solo cambia env runtime no fuerza update. Documentar en el runbook.
  • Background Sync en iOS Safari sigue sin existir (gotcha histórica). El fallback online listener es la única solución. No degradar la UX confiando en sync tag.
  • __APP_VERSION__ y los define-symbols rompen si el bundler hace tree-shake del fichero. Usar siempre los símbolos a través de version.ts, nunca inline.
  • updateViaCache: 'none' en el registro del SW evita que el navegador cachee el SW mismo agresivamente. Sin esto, el toast puede tardar hasta 24h en aparecer en algunos navegadores.

Scope explícito fuera

  • Push notifications (parked en MVP; iOS 16.4+ requiere instalación a home screen — fuera).
  • Resolución de conflictos con CRDT. Sigue siendo last-write-wins; solo se hace visible.
  • Migración a injectManifest (gotcha bloquea; sigue en generateSW).
  • Versionado semántico automático. La versión del package.json se bumpea a mano.