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.2 KiB
Fase 14 — PWA hardening (auto-update + offline polish + version chip)
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.
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.tsya llamaself.skipWaiting()yclientsClaim()— el SW nuevo toma control en cuanto entra (no requiere cerrar todas las pestañas).apps/web/src/routes/+layout.svelte:15registra conregisterSW({ immediate: true })sin callbacks. No hay UX para avisar al usuario de update.- Estrategia
generateSWactiva (ver gotcha "injectManifest hardcoded filename" en CLAUDE.md). Workbox precachea assets. - IndexedDB offline-first existe (CLAUDE.md "Sync Strategy").
sync_conflictsse 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 })auseRegisterSW({ onNeedRefresh, onOfflineReady })desdevirtual:pwa-register/svelte. - 14.1.2
UpdateToast.svelte— pill flotante bottom-center: "Nueva versión disponible — toca para actualizar" + botón "Recargar". Click invocaupdateSW(true)(force reload). - 14.1.3 Cuando
onOfflineReadydispara la primera vez (instalación inicial), toast distinto: "Lista para usar offline". Auto-dismiss a 4s. - 14.1.4 Telemetría mínima (
console.infocon prefijo[pwa]): registro, update detectado, update aplicado. Sin externalizar — solo facilitar debug de campo. - 14.1.5 Test Playwright
tests/e2e/pwa-update.test.tsPU-01: deploy mock con nuevo SW → toast aparece → click recarga → app sirve nuevo bundle. Stubs víaaddInitScriptpara simularonNeedRefresh.
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
MobileTopBaryDesktopSidebarcuando!navigator.onLine. Colorbg-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_conflictspara el usuario, banner amarillo en(app)/+layout.sveltecon 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-conflictslista la fila → "descartar local" la elimina.
- OF-01
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, inyectardefine: { __APP_VERSION__: JSON.stringify(<value>), __APP_COMMIT__: JSON.stringify(<value>) }:__APP_VERSION__=package.jsonversion 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.tsexporta{ version: __APP_VERSION__, commit: __APP_COMMIT__, builtAt: __BUILD_TIME__ }. Añadir__BUILD_TIME__ = new Date().toISOString()aldefine. - 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.svelteal final: chip pequeñotext-xs text-mutedconv{version} · {commit} · {builtAt.slice(0,10)}. No clicable, solo info. - 14.3.5 Bake durante el build de Docker (
apps/web/Dockerfile): pasarGIT_SHAcomo build arg, y desdeinfra/scripts/deploy-erosi.shcapturargit rev-parse --short HEADlocalmente 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íavi.stubGlobal; verifica queversionno seaundefinedni'dev'cuando el define está activo.
14.Z Verificación + cierre
- 14.Z.1
just test-allverde. 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
skipWaitingagresivo puede romper sesiones activas con cambios optimistas en vuelo. Mitigación: el toast pide confirmación del usuario antes deupdateSW(true). Si el usuario ignora, el SW nuevo entra de todos modos en la siguiente navegación — aceptable.onNeedRefreshno se dispara si Workbox no detecta cambios en el manifest. El build debe alterar al menos un asset hasheado o elrevisiondel precache no cambia.generateSWlo 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
onlinelistener es la única solución. No degradar la UX confiando ensynctag. __APP_VERSION__y los define-symbols rompen si el bundler hace tree-shake del fichero. Usar siempre los símbolos a través deversion.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 engenerateSW). - Versionado semántico automático. La versión del
package.jsonse bumpea a mano.