Hub

personal

tareas-hijo

active medium personal
Creado
2026-05-22
Actualizado
2026-05-30

Pendientes abiertos (3)

Ver todos →

🎯 Top de ataque (3)

  • #HIJ-003 📅 2026-06-22 (Fase 4, economía de puntos + premios) Schemas puntos_transacciones/premios/canjes, balance de puntos prominente en home hijo, catálogo + canjear, CRUD de premios en admin, cola de aprobación de canjes, ajuste manual de puntos. Deliverable: hijo acumula, canjea "30 min Switch", Sergio aprueba/marca cumplido. Estimación 1 sem. (antes #187) Gancho ya puesto: Tareas.aprobar/1 es el punto donde crear la transacción de puntos al aprobar.
  • #HIJ-004 📅 2026-06-30 🔥 (Fase 5, streaks + push) Query de streaks + badge en home hijo, VAPID + service worker, push diario al hijo si quedan pendientes (Oban cron), push a papá en eventos clave, settings de notifs en admin. Deliverable: hijo ve "🔥 7 días seguidos", recibe recordatorio si olvida tarea. Estimación 0.5 sem. (antes #188)
  • #HIJ-005 sin fecha (Fase 6, evolución continua) Lo que el uso real revele — dashboard semanal para papá, mejor offline, posibles segundos hijos, etc. Sin alcance fijo todavía. (antes #189)

Actividad en bitácora 7 días

jun
jul
ago
sep
oct
nov
dic
ene
feb
mar
abr
may
L
X
V
Menos
Más

App de tareas para el hijo de Sergio

Contexto

App para que el hijo de Sergio (pre-adolescente) lleve control de sus quehaceres y deberes diarios sin que Sergio le tenga que estar recordando. Objetivos principales:

  • Que el hijo vea de manera muy sencilla todo lo que tiene que hacer al día (tarea escolar, lavarse los dientes, limpiar su cuarto, alimentar a los perros, etc.) y lo pueda marcar como hecho.
  • Que Sergio observe en tiempo real qué ha completado.
  • Evidencia con foto para ciertas tareas (ej. limpiar escritorio → foto del escritorio limpio).
  • Validación de terceros para otras tareas (ej. lavarse dientes → la abuela, que lo cuida cuando Sergio trabaja, valida).
  • Objetivos claros del día/semana para que el hijo sepa qué falta antes de jugar.
  • UX extremadamente simple e intuitiva — marcar tarea hecha en 5-10 segundos máximo.

Sergio decidió construirlo en vez de usar herramienta existente (probó Joon brevemente) por dos razones: (1) ejercicio personal de aprendizaje, (2) moldearlo exactamente a su uso sin features sobrantes.

Decisiones de stack

Stack elegido (95% confirmado 2026-05-22): Phoenix LiveView + Elixir + Postgres, deploy probable en Fly.io.

Razones principales:

  • Multi-usuario en tiempo real (papá / hijo / abuela viendo y actualizando simultáneamente) es fit técnico ideal de LiveView — viene gratis vía PubSub, sin tocar WebSockets ni Pusher.
  • Sergio quiere aprender stack genuinamente distinto a Laravel+Inertia+Tailwind (lo que ya domina). Phoenix le da paradigma funcional + BEAM + LiveView de una sola apuesta.
  • live_upload para foto-evidencia sin escribir JS.

Alternativas evaluadas y descartadas: SvelteKit (cómoda pero menos aprendizaje), Expo+Supabase (nativa real pero dos cosas a la vez), Next.js (overkill + magia + filosofía “lo usan todos” no aplica para esta app), Rails+Hotwire/Django (muy parecidos a Laravel, aprendería poco arquitecturalmente).

Pendientes que decidía la sesión de arquitectura (#182) — todas resueltas 2026-05-22, ver ARCHITECTURE.md:

  • PWA mobile-first (sin wrap nativo) — hijo usa Android, push web funciona bien.
  • Offline: aceptamos limitación de LiveView (banner reconectando); en casa con wifi no bloquea. Mejora diferida.
  • Push: web_push_elixir con VAPID + service worker.
  • Modelo de datos: ledger de puntos + snapshots en transacciones + tareas_definicion/tareas_instancia + premios/canjes (ver schema completo en ARCHITECTURE.md).
  • Auth: custom liviano (NO mix phx.gen.auth) — magic link papá, PIN+dispositivo recordado hijo, sin cuenta abuela (links firmados).
  • Hosting: Fly.io (Postgres administrado + Tigris storage).
  • Validación abuela: deep link wa.me/ reenviado por papá (decisión 2026-05-23, reemplaza Meta Cloud API). Cero infra de mensajería, papá filtra antes de molestar a la abuela.
  • Recompensas: economía de puntos con catálogo de premios (canjes con aprobación de papá).

Tareas pendientes

Las fases vienen del plan de arquitectura. Estimación total: 5-5.5 semanas de tardes/findes.

  • (2026-05-24) #183 (Fase 0 ✅) Setup completo: mix phx.new tareas_hijo --database postgres --no-mailer vía container efímero, dev dockerizado (Dockerfile.dev + docker-compose.yml + Postgres 16-alpine), repo privado sevaor/tareas-hijo, deploy a Fly.io en dfw (Postgres unmanaged single-node + 4 secrets), Tailwind/esbuild funcionando, README dev local sin requerir Elixir en host. Live en https://tareas-hijo.fly.dev/ (welcome Phoenix 1.8.7). Subdominio custom tareas.sevaor.dev pendiente — no bloquea Fase 1.
  • (2026-05-28) #184 (Fase 1, MVP checklist ✅) Schemas+migrations + seed + LiveView /admin//hijo//papa + PubSub + auth completo (login PIN papá+hijo, sesión por token, remember device, role-gating, logout). Fase 1 cerrada — ver Resuelto 2026-05-28.
  • (2026-05-29) #185 (Fase 2, recurrencia + fotos ✅) Oban + cron diario (04:00 Cd. Juárez) + TZ-aware, live_upload con compresión client-side, Tigris bucket + URLs firmadas, galería en /admin/evidencias + miniaturas en /papa. 4 bloques + 1 fix commiteados/pusheados; verificado E2E en navegador (subida de foto OK tras fix del <form>+live_file_input). Ver bitácora 2026-05-29.
  • #HIJ-003 📅 2026-06-22 · 🤖 alto — (Fase 4, economía de puntos + premios) Schemas puntos_transacciones/premios/canjes, balance de puntos prominente en home hijo, catálogo + canjear, CRUD de premios en admin, cola de aprobación de canjes, ajuste manual de puntos. Deliverable: hijo acumula, canjea “30 min Switch”, Sergio aprueba/marca cumplido. Estimación 1 sem. (antes #187) Gancho ya puesto: Tareas.aprobar/1 es el punto donde crear la transacción de puntos al aprobar.
  • #HIJ-004 📅 2026-06-30 · 🤖 medio — (Fase 5, streaks + push) Query de streaks + badge en home hijo, VAPID + service worker, push diario al hijo si quedan pendientes (Oban cron), push a papá en eventos clave, settings de notifs en admin. Deliverable: hijo ve ”🔥 7 días seguidos”, recibe recordatorio si olvida tarea. Estimación 0.5 sem. (antes #188)
  • #HIJ-005 📅 sin-fecha · 🤖 alto — (Fase 6, evolución continua) Lo que el uso real revele — dashboard semanal para papá, mejor offline, posibles segundos hijos, etc. Sin alcance fijo todavía. (antes #189)

En progreso

Fases 1-3 cerradas. Fase 4 (#HIJ-003) parcialmente implementada en BD y contexto (schemas y transacciones listas), pendiente de conectar la UI de LiveView.

Estado del repo ~/code/tareas-hijo (al cerrar 2026-05-30)

  • origin/main en d5db33d = Mitad de Fase 4 (#HIJ-003), commiteada 2026-05-30. Working tree limpio. 50 tests verdes, compile limpio.
  • Pendiente para desplegar a prod: Fase 3 (validación abuela) y Fase 4 (tablas de recompensas) requieren correr migraciones add_validacion, create_premios, create_canjes, create_puntos_transacciones y add_puntos_otorgados_to_instancia en Fly al hacer fly deploy.
  • Tigris: bucket tareas-hijo-evidencias (1 solo, dev+prod con prefijo). Credenciales dev en ~/code/tareas-hijo/.env (gitignored, las puso Sergio); prod via Fly secrets.
  • Server dev en localhost:4000 (docker compose), Postgres en :5432. Seed con 5 tareas (2 piden foto, 1 pide validación de la abuela) + validador “Abuela” con número ficticio.

Cambio de modo de colaboración 2026-05-27

Tres etapas en un solo día:

  1. Madrugada-tarde: coach estricto (yo snippet, Sergio teclea). Cerraron bloques 0-3/4 Fase 1.
  2. Tarde (bloque 4/4): coach aligerado — yo edito archivos, Sergio corre comandos y commitea.
  3. Noche (2026-05-27): Sergio pidió deshabilitar el modo coach completo. A partir de ahora tareas-hijo se trata como cualquier otro proyecto suyo: yo edito archivos, corro comandos, valido y reporto. Commit/push siguen auth per-sesión como regla general del hub. Memoria [[tareas-hijo-learning-mode]] actualizada para reflejarlo.

Resuelto 2026-05-30 [#HIJ-003 (Fase 4 - mitad)]: Schemas y lógica core de Economía de Puntos y Premios. Decisiones de diseño (Challenge Assumptions): Se mantuvo el modelo en Gemini 3.1 Pro por ser tarea arquitectural pesada. Se crearon los schemas Premio, Canje y Transaccion mediante mix phx.gen.schema (sin context mágico). Se añadió puntos_otorgados al schema de Instancia para asegurar inmutabilidad histórica. Context Recompensas: Escrito manualmente con CRUD de premios, query balance_puntos/1, y solicitar_canje/aprobar_canje manejados con Ecto.Multi para transaccionalidad atómica real. Refactor Gancho Puntos: Se actualizó Tareas.actualizar_y_broadcast/2 usando Ecto.Multi — si la tarea pasa a estado :hecha o :aprobada, se registra el snapshot de los puntos y se crea la transacción en el ledger en el mismo viaje a la base de datos. Verificación: 50 tests verdes (ninguna regresión en validacion_test), mix compile --warnings-as-errors limpio. Commiteado localmente (d5db33d). Falta (próxima sesión): Conectar estas funciones de Recompensas a los 3 LiveViews (/hijo, /admin, /papa).

Resuelto 2026-05-30 [#HIJ-001 (Fase 3) + #HIJ-002]: Validación de tareas por tercero (la abuela) vía deep link de WhatsApp, sin Meta Cloud API. Modelo Opus 4.8 + effort alto (tag 🤖 alto). Decisión de diseño (Challenge Assumptions): el deliverable decía “push web a papá”, pero la infra de web push (VAPID/SW) es de Fase 5 (#HIJ-004); en vez de adelantarla, la notificación al papá se entrega como cola “Por validar” en vivo en /papa (ya suscrito al PubSub del hijo). El push del SO se sumará en Fase 5. Migración 20260530160000_add_validacion: tablas validadores (nombre + whatsapp_e164 + activo) y validacion_links (token aleatorio 32B + expira_at 24h + usado_at + resultado); tareas_definicion += requiere_validacion + validador_id (FK nilify); tareas_instancia += validador_id (copiado de la def al materializar) + aprobada_at/rechazada_at/motivo_rechazo; estado de instancia ampliado a [:pendiente, :hecha, :en_validacion, :aprobada, :rechazada] (la columna ya era :string, solo cambió el Ecto.Enum). Schemas nuevos: Validador (normaliza E.164 quitando espacios/guiones + valida formato), ValidacionLink (nuevo_changeset genera token + 24h, consumir_changeset, vigente?/1). Context (tareas.ex, +276 líneas): toggle_estado/1 → verbos explícitos marcar_hecha/desmarcar/reintentar (decide :hecha vs :en_validacion según requiere_validacion); adjuntar_evidencia_y_marcar igual; lista_por_validar/1 (cola FIFO), aprobar/1 (sella aprobada_atgancho de puntos de Fase 4 va aquí), rechazar/2 (motivo opcional); CRUD validadores; crear_validacion_link/1 (:sin_validador si la instancia no tiene validador), get_validacion_link/1 (preloads), resolver_validacion/3 (consume el link con guarda atómica update_all where is_nil(usado_at) a prueba de doble-tap + chequeo de vigencia). Helper privado actualizar_y_broadcast reusa el patrón update→re-pega preloads→PubSub. UI: hijo_live reescrito (5 estados: marcar/desmarcar/⏳en validación/✓aprobada/✗rechazada+Reintentar); papa_live con sección ”🔔 Por validar” (foto + 3 acciones: Aprobar yo / 📤 Pedir a abuela / Rechazar con form de motivo inline) + badges de estado en la lista del día; admin_live + gestión de validadores (tabla + form inline) y el form de definiciones gana checkbox requiere_validacion + select de validador; definicion_form_component actualizado. Controller ValidacionController.pedir_validacion_wa → genera link + 302 a https://wa.me/<e164>?text=<urlencoded>, mensaje con placeholders {validador}{hijo}{tarea}{url} configurable por env MENSAJE_PEDIR_VALIDACION (default en español). Vista pública /v/:token (ValidacionPublicaLive, live_session sin auth, layout: false): grande y simple para la abuela — foto + 2 botones Sí/No; maneja link inválido/usado/vencido. Plug nuevo require_papa en user_auth para el endpoint del controller. #HIJ-002: nav entre /papa/admin/admin/evidencias en ambos headers. Verificación: 50 tests verdes (24 de context en validacion_test.exs + 9 de integración LiveView/controller en validacion_flujo_test.exs que ejercitan live/3+render_click el flujo real de la abuela y la cola del papá + 17 previos), mix compile --warnings-as-errors limpio, migración aplicada en dev+test, smoke en server dev (/v/ inválido renderiza mensaje, gating auth intacto). Seed actualizado: validador “Abuela” (número ficticio +525555555555, Sergio lo cambia en /admin) + tarea “Lavarse los dientes (noche)” con validación. Commiteado+pusheado a origin/main (c6c4cb9) con autorización de Sergio. Ver [[tareas-hijo-learning-mode]] (modo coach OFF, yo escribí y validé todo).

Resuelto 2026-05-28 [#184 bloque 4/4 + Fase 1 COMPLETA]: Auth liviano custom (sin mix phx.gen.auth) reescrito desde cero en la WSL (el 4/4 de la PC de casa nunca se pusheó). 6 archivos nuevos: lib/tareas_hijo/cuentas/user_token.ex (schema users_tokens + build_session_token/1 con SHA-256 del token aleatorio, verify_session_token_query/1 con validez 60 días), lib/tareas_hijo_web/user_auth.ex (plugs fetch_current_user/require_authenticated_user/redirect_if_user_is_authenticated + log_in/log_out con renew_session anti-fixation + cookie persistente _tareas_hijo_web_user_remember_me 60d + hooks on_mount :ensure_authenticated/:ensure_papa/:ensure_hijo + signed_in_path/1 por rol), session_controller.ex + session_html.ex + session_html/new.html.heex (login con 2 botones de rol + PIN + recordar dispositivo), migración 20260528180000_create_users_tokens.exs. 9 archivos modificados: mix.exs (+bcrypt_elixir ~> 3.0), user.ex (campo virtual :pin + pin_changeset/2 con bcrypt + valid_pin?/2 con no_user_verify anti-enumeración), cuentas.ex (get_user_by_rol_and_pin/2, set_pin/2, generate/get_by/delete_user_session_token; se conserva get_hijo! que papa_live sí necesita), router.ex (fetch_current_user en pipeline + live_session :hijo/:papa con on_mount por rol + rutas /login//logout), hijo_live.ex/papa_live.ex/admin_live.ex (retiro de mocks get_hijo!/get_papa!current_user desde on_mount + link de logout), seeds.exs (PINs dev: papá 2468, hijo 1234), config/test.exs (hostname desde DB_HOST env para correr tests dockerizados). Fix del compile error que bloqueaba en casa: redirect/2/put_flash/3 son ambiguos entre Phoenix.LiveView y Phoenix.Controller; se importa solo Phoenix.Controller (para los plugs) y dentro de los on_mount se usa el prefijo completo Phoenix.LiveView.redirect/2/put_flash/3. Verificación: mix compile --warnings-as-errors limpio (sin aliases muertos), mix test 5/5 verdes, y flujo de auth validado E2E con curl (8 escenarios): /hijo sin sesión→/login; login hijo 1234→/hijo y sesión válida; role-gating hijo→/papa redirige a /hijo; PIN incorrecto→/login sin sesión; login papá 2468→/papa y /admin 200; gating inverso papá→/hijo redirige a /papa; logout DELETE→/login con token borrado en BD (verificado count users_tokens baja). Modo coach OFF: yo escribí todo el código, corrí todos los comandos y validé. ⚠️ Working tree sin commit, pendiente autorización de Sergio para commit+push. Cuando Sergio vuelva a la PC de casa debe descartar el 4/4 viejo divergente con git reset --hard origin/main && git pull.

Resuelto 2026-05-27 [#184 bloque 3/4]: LiveView /admin con CRUD tareas_definicion completo. 1 archivo nuevo: lib/tareas_hijo_web/live/admin_live.ex (LV padre, tabla + handle_info para {DefinicionFormComponent, {:saved, def}} / :cancelado, handle_event “edit” + “toggle_activa”, helpers recurrencia_label/1 y dias_label/1). 1 archivo nuevo: lib/tareas_hijo_web/live/definicion_form_component.ex (LiveComponent reusable, update/2 + handle_event “validate”/“save”/“cancelar”, dispatch save_definicion/3 con head para :new y :edit, notify_parent con send(self(), {__MODULE__, msg}), helper dias_seleccionados/1 para normalizar int/string del form). 1 edit context: 5 funciones nuevas en lib/tareas_hijo/tareas.ex sección “Definiciones (CRUD admin)” — lista_definiciones/0 (sin filtro activa, ordenadas por nombre), change_definicion/2 (changeset suelto con default attrs \\ %{}), crear_definicion/1 y actualizar_definicion/2 (pipeline estándar |> Definicion.changeset(attrs) |> Repo.insert/update), toggle_activa/1 (con Ecto.Changeset.change(activa: not def.activa) — atajo quirúrgico que esquiva validate_required completo). 1 edit router: 1 línea live "/admin", AdminLive. Decisiones aterrizadas: (a) single-page layout (no /admin/tareas/nueva y /admin/tareas/:id/edit) — tabla arriba + form siempre visible al pie; (b) toggle activa en lugar de delete — on_delete: :delete_all del schema reventaría histórico que Fase 4 necesita para puntos; (c) sin broadcast cross-LV de cambios de definiciones — el catálogo es separado de las instancias del día ya materializadas; (d) form con 7 checkboxes a mano para dias_semana (en vez de <.input type="checkbox">) porque <.input> inyecta hidden value="false" que rompe arrays HTML — name="definicion[dias_semana][]" directo; (e) Map.put(:action, :validate) en el changeset durante handle_event("validate", ...) para forzar mostrar errores antes de Repo.insert. Gotchas didácticos: LiveComponent NO tiene proceso propio (corre dentro del LV padre) → self() ES el pid del padre → send(self(), msg) le habla al jefe; phx-target={@myself} enruta events al componente y no al padre; to_form(changeset, as: :definicion) hace que names HTML sean definicion[nombre] y desestructure encaje con %{"definicion" => params}; Calendar.strftime/2 formatea %Time{} sin Timex; data-confirm="..." dispara confirm() del browser antes del phx-click sin escribir JS; el patrón notify_parent con namespacing {__MODULE__, msg} evita colisión cuando el padre embebe múltiples componentes. Smoke test E2E cross-LV (3 pestañas /admin + /hijo + /papa en paralelo): crear/editar/desactivar defs en /admin confirmado NO refresca /hijo ni /papa automáticamente — aterriza diferencia catálogo (definiciones) vs día (instancias materializadas). Toggle estado en /hijo sigue refrescando /papa al instante (PubSub del bloque 2/4 intacto). Limitación conocida observada: defs nuevas creadas a mitad del día NO entran al “hoy” del hijo (lazy generation solo materializa si lista_del_dia está vacía); Fase 2 #185 resuelve con Oban cron. Modo coaching estricto 100%: Sergio escribió todo el código y corrió todos los comandos.

Resuelto 2026-05-26 [#184 bloque 2/4]: LiveView /hijo + /papa end-to-end con PubSub. 3 archivos nuevos: lib/tareas_hijo/tareas.ex (context con subscribe/1 + topic/1 + broadcast/2 + lista_del_dia/2 + obtener_o_generar_del_dia/2 + materializar_dia/2 + toggle_estado/1), lib/tareas_hijo/cuentas.ex (helpers get_hijo!/0 + get_papa!/0 para mock pre-auth), lib/tareas_hijo_web/live/hijo_live.ex + papa_live.ex. Router: 2 rutas live agregadas (/hijo, /papa) al pipeline browser. Decisiones: (a) lazy generation de tareas_instancia en mount (Fase 2 lo reemplaza con Oban cron 00:00) — Repo.insert_all/3 con on_conflict: :nothing sobre el unique (def_id, user_id, fecha) para race protection entre pestañas; (b) Date.utc_today() por ahora con TODO para TZ-aware (memoria [[feedback-timezone-cd-juarez]]) en Fase 2 con tzdata; (c) :una_vez y :manual inertes en Fase 1 (las dispara admin/cron después); (d) PubSub topic por user "tareas:user:#{user_id}" con broadcast privado al context (regla C10: “context emite, LV escucha”); (e) marcar_hecha/1toggle_estado/1 durante la sesión cuando Sergio reportó “no puedo desmarcar las 2 hechas del testing IEx” — un solo verbo más simple, pattern matching case instancia.estado do :pendiente -> ... :hecha -> ... end; (f) re-pegado de tarea_definicion post-Repo.update (preload se descarta) antes del broadcast para que el LV reciba la struct lista para renderizar. Gotchas didácticos: connected?(socket) guard en mount (corre 2x HTTP+WS, suscribir solo en WS); from(i in Instancia, ..., join: d in assoc(i, :tarea_definicion), preload: [tarea_definicion: d]) reusa el join en vez de hacer N+1; Repo.insert_all con schema (no string) sí castea Ecto.Enum; ^dia_iso in d.dias_semana se compila a $1 = ANY(d.dias_semana) operador Postgres nativo. Validación E2E: 3 pestañas (/hijo × 2 + /papa) abiertas, toggle en una se refleja al instante en las otras dos sin recargar — primera vez que Sergio ve PubSub aterrizado (C10) tras la sesión teórica del 2026-05-23. Modo coaching estricto 100%: Sergio escribió todo el código y corrió todos los comandos. Bug atrapado en la sesión: el botón inicial tenía disabled={tarea.estado == :hecha} y marcar_hecha/1 solo iba pendiente→hecha — al refrescar había 2 hechas residuales del testing IEx imposibles de quitar; el toggle resuelve ambos problemas en un solo cambio.

Resuelto 2026-05-24 [#183]: Fase 0 cerrada — app viva en https://tareas-hijo.fly.dev/. Stack en local: Phoenix 1.8.7 + Postgres 16-alpine completamente dockerizado (Dockerfile.dev + docker-compose.yml + bind mount puro para que ElixirLS vea deps/), config/dev.exs apunta a hostname db. En prod: Dockerfile multi-stage (elixir:1.17-otp-27 builder → debian:bookworm-slim runner) + fly.toml con primary_region=dfw, internal_port=8080, auto_stop_machines + Postgres unmanaged single-node attached + 4 secrets (SECRET_KEY_BASE, PHX_HOST=tareas-hijo.fly.dev, ECTO_IPV6=true, DATABASE_URL). Repo privado sevaor/tareas-hijo con 2 commits. Modo coaching mantenido al 100%: Sergio escribió todo el código y corrió todos los comandos; yo solo dí snippets + explicaciones. Subdominio custom tareas.sevaor.dev queda como tarea aparte (no bloquea Fase 1).

Resuelto 2026-05-22 [#182]: Sesión completa de arquitectura. Stack final confirmado (Phoenix LiveView + Postgres + Oban + Fly.io + Meta Cloud API + Tigris). Output: ARCHITECTURE.md con modelo de datos completo, flujos principales (F1-F6), integraciones externas, PWA setup, hosting, seguridad, riesgos y plan de 6 fases con IDs #183-#189. Decisiones clave: PWA Android-first (sin wrap), economía de puntos con catálogo, validación abuela vía link único firmado en WhatsApp (sin app que aprender), auth custom liviano (no mix phx.gen.auth), Tigris para fotos con URLs firmadas, ledger append-only de puntos con snapshots en transacciones. Inputs de Sergio capturados: hijo usa Android, edad 9-11, abuela valida vía WhatsApp aprobar/rechazar, economía de puntos sí. Estimación total 5-5.5 semanas tardes/findes.

Notas técnicas

Recursos de aprendizaje recomendados a Sergio

  • Libro: “Programming Phoenix LiveView” (Bruce Tate & Sophie DeBenedetto, Pragmatic) — el más actualizado.
  • Curso: Pragmatic Studio “Phoenix LiveView”.
  • Elixir School (free, online) para el lenguaje.
  • ElixirCasts.io — screencasts cortos.
  • Discord oficial de Elixir — comunidad chica pero muy responsiva.

Curva de aprendizaje estimada (viniendo de Laravel sólido)

  • Semana 1-2: pelea con sintaxis e inmutabilidad.
  • Semana 3-4: clic con pipes y pattern matching.
  • Mes 2-3: LiveView se siente obvio.
  • Mes 6+: OTP natural.

Para esta app específica: estimación inicial 4-6 semanas de tardes/findes (vs ~2 semanas con Laravel — el delta es el costo de aprendizaje).

Características técnicas que LiveView regala para este caso

  • Real-time multi-usuario vía Phoenix.PubSub — el hijo marca tarea → mamá y papá ven el cambio al instante sin escribir JS ni WebSocket handlers.
  • live_upload con preview/progress/drag-and-drop sin JS.
  • Hot reload instantáneo en dev.
  • mix phx.gen.auth genera auth completa en un comando.
  • Tailwind viene integrado out of the box en Phoenix 1.7+.

Puntos débiles conocidos para este caso

  • Offline: LiveView depende del WebSocket — para offline real se necesita estrategia híbrida (cachear estado, sync al reconectar). No bloqueante (la app se usará en casa con wifi) pero hay que diseñarlo.
  • Push notifications: no son tan inmediatas como Expo nativo; hay que sumar librería tipo web_push_elixir.
  • Hosting: no es “subir a cualquier VPS”; lo más limpio es Fly.io (built-in para Elixir) o self-hosted con mix release.

Bitácora

2026-05-22

  • Pidió: platicar si construir vs usar herramienta existente; después, opciones de stack distinto a Laravel; después, profundizar en Phoenix LiveView.
  • Hice: evaluación honesta (mencioné Joon, OurHome, S’moresUp como existentes); recomendé probar Joon primero. Sergio probó y decidió construir como ejercicio personal. Pasé 4 opciones de stack (SvelteKit, Phoenix LiveView, Expo+Supabase, Next.js) con pros/contras y mi orden de preferencia para su caso. Sergio pidió profundizar en Phoenix LiveView: expliqué Elixir/BEAM/Phoenix/LiveView en capas, mostré snippet de “marcar tarea como hecha” con PubSub multi-usuario, listé lo que tendría que reaprender (inmutabilidad, pattern matching, with, no null, procesos vs threads, supervisores), ecosistema (bueno: mix/hot reload/LiveDashboard/Tailwind/Ecto/gen.auth; menos bueno: comunidad chica, hosting distinto), fit específico para esta app, curva de aprendizaje realista, recursos. Sergio confirmó 95% Phoenix LiveView.
  • Falta: sesión completa de arquitectura (#182) en próxima conversación — modelo de datos, auth multi-rol, evidencia con foto, push, PWA vs wrap, hosting.

2026-05-22 (noche tardía — sesión de arquitectura #182)

  • Contexto previo: sesión arrancó con “Vamos a seguir con #182” desde la PC personal. Hub local estaba en estado roto: 4 objetos git loose vacíos por crash de WSL2 a las 02:18 + ref main apuntando al objeto corrupto. Working tree intacto. Recovery: backup defensivo de .git, update-ref al último commit bueno (19141cac), borrar 4 loose vacíos, limpiar reflogs/cache-tree corruptos, rebuild de .git/index. Después: git ls-remote reveló que origin estaba 35 commits adelante (Sergio había pusheado desde otra máquina entre crash y ahora) — todos los commits locales ya estaban replicados allá con SHAs distintos. Fast-forward de 35 commits, descarte del cambio local huérfano de memory/MEMORY.md (ya cubierto por origin), backup borrado. Hub limpio, sin push pendiente.
  • Pidió: “Vamos a seguir con #182” → diseño de arquitectura completa para arrancar Fase 0.
  • Capturé: 4 inputs decisivos vía AskUserQuestion — teléfono hijo (Android), edad (9-11), abuela (WhatsApp aprobar/rechazar + foto, sin app), recompensas (economía de puntos con catálogo).
  • Hice: documento tareas-hijo/ARCHITECTURE.md (≈350 líneas) con stack final, roles y auth, modelo de datos completo con 9 tablas y decisiones de modelado, 6 flujos principales detallados (marcar simple, con foto, validación papá, validación abuela vía WhatsApp, canje de premio, generación diaria), integraciones (Meta Cloud API + Web Push + Tigris), PWA (manifest + service worker + install prompt), estrategia offline, hosting Fly.io con estimación de costo ($0-5/mes), seguridad (PII, HMAC, rate limit), riesgos con mitigaciones, y plan de 6 fases con scope y deliverable explícito. IDs #183-#189 abiertos en sección “Tareas pendientes” mapeando 1:1 a las fases. Cerrado #182.
  • Decisiones técnicas que pueden sorprender después si no se anotan acá:
    • No mix phx.gen.auth — overkill para 2 cuentas reales; magic link papá + PIN hijo + sin cuenta abuela.
    • Abuela no es users row — vive en tabla validadores separada; el hub queda extensible (la maestra, el papá del fútbol, etc.) sin refactor.
    • Ledger append-only para puntos (no columna balance) — soporta ajustes manuales, recálculo seguro, audit trail.
    • Snapshots de costo/puntos en transacciones — cambiar definición/precio después no rompe historia.
    • Puntos se descuentan en aprobado, no en solicitado — evita “puntos en limbo” si papá deniega canje.
  • Falta: arrancar Fase 0 (#183) en próxima sesión — mix phx.new + Fly.io + Postgres + Tigris + “hola Leonardo” deployado. Dejé reservado el slug del repo tareas_hijo (snake_case Elixir) y del subdominio tentativo tareas.sevaor.dev (Sergio confirma cuando arranque Fase 0).

2026-05-23

  • Pidió: antes de arrancar Fase 0, cuestionar si todo el setup de Meta Cloud API era necesario; preguntó si había manera más simple de que un mensaje llegue a su mamá para validar.
  • Hice: propuse 4 alternativas vía AskUserQuestion (deep link wa.me/ con papá como puente / solo papá valida sin abuela / Telegram Bot oficial / email a abuela). Sergio eligió la 1.
  • Cambio de arquitectura: Meta Cloud API descartada para esta app. La validación de la abuela se hace ahora vía deep link wa.me/<e164>?text=<urlencoded> que abre desde el push notif que Sergio recibe. Sergio reenvía manualmente con un tap (mensaje prerelleno). El mensaje sale de su WhatsApp personal, no de un número de bot.
  • Implicaciones:
    • Fase 0 (#183) deja de tener “trámite paralelo de template Meta” — bloque eliminado.
    • Fase 3 (#186) reescrita: ya no Oban worker EnviarValidacion + template + phone number nuevo. En su lugar: endpoint /admin/pedir-validacion-wa/:task_id + push web a papá con 3 acciones (aprobar yo / pedir a abuela / rechazar). Estimación bajó de 1 sem a 0.5-1 sem.
    • F4 en ARCHITECTURE.md reescrito completo (paso 2-5 reemplazados). Sección “Integraciones externas” cambió “Meta Cloud API” por “WhatsApp vía deep link wa.me/”. Riesgo “aprobación de template Meta tarda” eliminado; nuevo riesgo “papá no disponible para reenviar” con mitigación (las tareas se acumulan en cola del push).
    • Estructura de tablas (validadores, validacion_links) SIN cambios — sigue soportando migración futura a Meta Cloud API si en uso real el papá-como-puente no escala.
  • Ventaja UX no obvia: mensaje sale del WhatsApp personal de Sergio, no de un número desconocido — para la mamá es “es Sergio quien me escribe”, flujo natural; con Meta API sería “+52xxx desconocido te envió un template” que mucha gente ignora.
  • Falta: sin cambios al plan inmediato. Próxima sesión sigue siendo arrancar Fase 0 (#183).

2026-05-23 (tarde — C5 Mix tooling)

  • Pidió: “Vamos a seguir en lo que nos quedamos de tareas-hijo.” Confirmó vía AskUserQuestion seguir el orden del mapa: C5 — Mix tooling.
  • Hice: sección C5 completa en LEARNING.md (~190 líneas) con 12 subsecciones: qué es Mix vs composer/artisan/npm scripts, mix.exs como composer.json + service provider, tareas core (deps/compile/test/help), tareas Phoenix y Ecto (generadores, ecto.*), distinción “generadores escupen código tuyo, no magia runtime”, iex -S mix phx.server como reemplazo de tinker + servidor en vivo, mix release para Fly.io, aliases como scripts.json, mix.lock, configuración por entorno con la trampa prod.exs vs runtime.exs (vars de entorno deben ir en runtime.exs o se hornean en build vacías), tareas custom propias y cheatsheet diario. Índice marcado [x]; sección “Próxima sesión” actualizada para retomar en C6 — Estructura Phoenix (lib/app/ vs lib/app_web/, contexts).
  • 3 puntos no obvios resaltados a Sergio: (1) iex -S mix phx.server = REPL dentro del servidor corriendo, Laravel no lo tiene; (2) config/runtime.exs para env vars en prod, no prod.exs; (3) generadores Phoenix dejan archivos tuyos sin registro oculto — borrarlos elimina la feature limpiamente.
  • Falta: próxima sesión retomar en C6 o decidir cuándo abandonar el mapa para arrancar Fase 0 (#183). Modo estricto “Sergio escribe, Antigravity explica” sigue vigente.

2026-05-23 (noche — sesión maratón C6-C10, mapa cerrado)

  • Pidió: sesión nueva, “vamos a seguir con C6, explicame a fondo aqui en el chat, como en la sesion pasada”. Después en cadena: pasar a archivo + arrancar C7 → C8 → C9 → C10 sin descansos. Modo estricto “Sergio escribe, Antigravity explica” mantenido todo el camino.
  • Hice: 5 capas explicadas a fondo primero en chat y después persistidas en LEARNING.md. Estructura uniforme por capa: subsecciones numeradas + paralelos Laravel + trampas mentales + ejemplos concretos con TareasHijo.*. Total agregado al archivo: ~1,500 líneas. Cierre del mapa con sección ”🎉 Mapa del territorio completo” listando las 10 capas con resumen 1-línea cada una.
    • C6 — Estructura Phoenix: árbol que mix phx.new deja, split lib/app/ (negocio) vs lib/app_web/ (HTTP), convención naming módulo↔archivo, contexts como API entre _web y BD, diagrama de flujo de request, archivos top-level app.ex + app_web.ex con __using__ macros, dentro de _web/, priv/, test/, application.ex preview, propuesta de 3 contexts para tareas-hijo (Cuentas/Tareas/Recompensas), cheatsheet Laravel→Phoenix.
    • C7 — Ecto: las 4 piezas (migration/schema/changeset/repo) y por qué separadas; Data Mapper vs Active Record como clave mental; migrations con change/0; schemas con campos virtuales y asociaciones que NO cargan auto; changesets como pipeline (cast whitelist + validate_* + unique_constraint); Repo como única puerta; Query DSL con macros y pinning ^var; preload explícito; transacciones con Multi; constraints vs validations; embedded schemas; ejemplo realista completo de Instancia con migration+schema+context+LV; cheatsheet Eloquent→Ecto.
    • C8 — LiveView lifecycle: flujo completo (HTTP + WS), mount/3 corre 2x con connected? guard, handle_params para cambios de URL sin recarga, render/1 + HEEx con {expr} moderno, handle_event del cliente, handle_info de procesos (conecta C1+C10), assigns y diffing, forms con to_form + phx-change="validate", streams para listas grandes, LiveComponent vs function component, push_navigate vs push_patch vs redirect.
    • C9 — Supervisores + “let it crash”: filosofía contra-PHP/JS, procesos como aislamiento, supervisor tree, Application.start/2 como raíz, child specs, 3 estrategias de restart (one_for_one/one_for_all/rest_for_one), max_restarts/max_seconds como red de seguridad, GenServer básico con call/cast, DynamicSupervisor, encaje de Phoenix en el árbol, panorama completo del árbol de tareas-hijo, cuándo try/rescue SÍ tiene sentido.
    • C10 — PubSub: qué es (y qué NO es vs Redis/Pusher/MQ), arquitectura con process groups, los 3 verbos (subscribe/broadcast/unsubscribe), handle_info como integración, estrategia de tópicos (granularidad), variantes (broadcast/broadcast_from/local_broadcast), distribuido entre nodos BEAM, patrón “context emite, LV escucha”, caso real completo de tareas-hijo (hijo marca → papá ve → papá aprueba → hijo ve puntos), uso con uploads y push_event al cliente, comparación con stack Laravel (Redis + Pusher + Echo → 3 líneas Elixir).
  • Cosa importante para Sergio: modo cambia. Próxima sesión deja de ser “Sergio escribe, Antigravity explica” y pasamos a co-construcción real con el modelo mental en su lugar. Empieza con Fase 0 (#183).
  • Falta: arrancar Fase 0 (#183) — mix phx.new tareas_hijo --database postgres --no-mailer, Fly.io app + Postgres provisionados, “hola Leonardo” deployado en subdominio (tentativo tareas.sevaor.dev), Tailwind funcionando, repo GitHub privado. Estimación 0.5 sem. Sergio confirma subdominio cuando arranque.

2026-05-24 (madrugada — Fase 0 #183 cerrada)

  • Pidió: “Vamos a empezar con #183. Antes de empezar, quiero que el desarrollo local sea con docker, para no tener que instalar nada en mi maquina y sea facil retomar en la computadora del trabajo.” Después: “Recuerda que este proyecto lo quiero hacer yo, escribirlo a mano y que me vayas explicando las cosas especificas de Elixir/Phoenix… Yo quiero correr los comandos.” → modo coaching estricto.
  • Memorias guardadas al inicio: [[keep-repos-synced]] (regla: git fetch al iniciar sesión hub + git pull --ff-only antes de tocar ~/code/<slug> — varias máquinas) y [[tareas-hijo-learning-mode]] (regla específica: en este proyecto Sergio escribe TODO el código y comandos; yo solo doy snippet + comando + explicación, NO uso Write/Edit en el proyecto ni ejecuto mix/docker compose por él).
  • Flujo de la sesión (Sergio teclea, yo coachéo):
    • Setup local: mkdir ~/code/tareas-hijo, generar Phoenix con docker run --rm --user $(id -u):$(id -g) -e HOME=/tmp -v $PWD:/app elixir:1.17-otp-27 sh -c "mix local.hex --force && ... && echo Y | mix phx.new . --app tareas_hijo --module TareasHijo --database postgres --no-mailer --no-install" (Phoenix 1.8.7 generado).
    • Tour de archivos clave (mix.exs, application.ex, router.ex, dev.exs) con paralelos a C9 LEARNING.md (supervisor raíz) y notas sobre bandit-no-cowboy.
    • Dockerización dev: Dockerfile.dev (user no-root UID 1000 para evitar archivos root-owned), docker-compose.yml (Postgres 16-alpine + healthcheck + depends_on service_healthy), edit config/dev.exs hostname localhostdb.
    • Rename mastermain, README dev local sin referencia a Elixir/Postgres en host, commit inicial 9e7bd1d.
    • GitHub: apt install gh (repo oficial) + gh auth login SSH + gh repo create tareas-hijo --private --source . --remote origin --push.
    • Fly: curl https://fly.io/install.sh | sh + auth → app tareas-hijo en org personal + 3 secrets staged + Postgres unmanaged tareas-hijo-db en dfw (Querétaro NO existe en Fly, lo más cercano a Cd. Juárez es Dallas) + attach → DATABASE_URL inyectado. fly.toml a mano (auto_stop_machines stop, min_machines_running 0 para hobby costo ~$0). Deploy.
  • 3 gotchas que costaron tiempo, capturadas en memoria [[phoenix-fly-setup-gotchas]]:
    1. Docker compose named volumes anidados en bind mounts → al primer arranque con deps:/app/deps + build:/app/_build, Docker creó los mount points como root en el host filesystem. Aunque down -v borre los volúmenes, los dirs quedan huérfanos root-owned y la siguiente vez (con bind mount puro) el container no puede escribir → (File.Error) could not make directory (with -p): no such file or directory cryptic. Fix: sudo rm -rf deps _build + bind mount puro (en WSL2 con código en filesystem Linux tiene performance nativa y bonus de IDE ve deps/ para ElixirLS).
    2. flyctl launch compila con Elixir del host — Sergio tenía Elixir 1.12 del sistema, Phoenix 1.8 pide 1.15+ → launch revienta antes de generar fly.toml/Dockerfile. Saltado el scanner: setup manual con mix phx.gen.release --docker dentro del container + fly.toml a mano + flyctl apps create sin scanner + secrets + postgres create/attach + deploy.
    3. Phoenix 1.8 colocated hooks — esbuild en mix assets.deploy falla con Could not resolve "phoenix-colocated/tareas_hijo" si mix compile no corrió antes. El compiler :phoenix_live_view genera el dir _build/.../phoenix-colocated/<app>/ durante compile, y esbuild lo necesita. Mi Dockerfile inicial tenía assets.deploy antes de compile (template Phoenix 1.7 viejo); invertir solucionó.
  • Bonus gotcha: mix phx.gen.release --docker consulta Docker Hub API en runtime para fijar el tag exacto del builder (hexpm/elixir:X-erlang-Y-debian-bookworm-SNAPSHOT-slim). Docker Hub dio 504 → task abortó dejando release.ex y scripts shell pero sin Dockerfile. Solución: Dockerfile a mano con imagen oficial elixir:1.17-otp-27 (ya validada en dev, no requiere pin de snapshot Debian).
  • Live: https://tareas-hijo.fly.dev/ sirve welcome Phoenix 1.8.7 desde Dallas. Commit final <hash> en sevaor/tareas-hijo con Dockerfile prod + fly.toml + release.ex + rel/overlays/bin/{server,migrate}.
  • Falta: #184 (Fase 1, MVP checklist) — schemas+migrations + auth papá+hijo + LiveView /admin y /hijo + PubSub. Modo coaching sigue vigente. Subdominio custom tareas.sevaor.dev queda como tarea menor (no bloquea Fase 1; configurable después con flyctl certs add).

2026-05-24 (noche — #184 bloque 1/4: schemas + migrations + seed)

  • Pidió: “Vamos a avanzar hoy con el pendiente #184, recuerda seguir en modo coach”. Modo coaching estricto se mantuvo todo el bloque (Sergio escribió 100% del código y corrió 100% de los comandos; yo solo dí snippets + comandos + explicaciones).
  • Decisiones de arranque (antes de tocar código):
    • PIN papá en vez de magic link email. Magic link real implicaría Swoosh + adaptador SMTP (Gmail/Postmark) + token controllers + plantilla HTML + secret en Fly + rate-limit. Eres tú solo desde 2-3 dispositivos — overhead injustificado para v1. PIN papá reusa el mismo mecanismo del hijo (diferente rol). Cuando llegue otro admin que justifique SMTP, se cambia.
    • Scope mínimo de columnas en Fase 1. Diferidos a sus respectivas fases con migration propia: push_subscription y avatar_url (Fase 5 push), evidencia_url y requiere_foto (Fase 2 fotos), requiere_validacion y validador_id (Fase 3 validación abuela), puntos_otorgados (Fase 4 puntos). Patrón: cada fase agrega sus columnas con mix ecto.gen.migration. Beneficio: enseña el ritmo “alter table via migration” que es central en Ecto y mantiene commits limpios por fase.
    • Namespace de contexts: Cuentas (users + remembered_devices) · Tareas (definicion + instancia) · Recompensas (futuro). Aterriza C6 — el split físico lib/tareas_hijo/cuentas/, lib/tareas_hijo/tareas/.
    • Generador mix phx.gen.schema (no gen.context). gen.schema genera solo migration + schema, sin context wrapper. Sergio escribirá el context a mano más adelante (bloque 4 o aparte) para entender qué hace cada pieza en vez de aceptar el wrapper mágico que escupe Phoenix.
    • Enums en BD como string + Ecto.Enum en schema (no CREATE TYPE). Más flexible — agregar valor es editar lista en código, sin migration. Idiomático Phoenix moderno.
  • Flujo del bloque (Sergio teclea, yo coachéo):
    • Setup: docker compose up -d para db (el servicio en docker-compose.yml se llama app, no web — corregido en marcha; usé web inicial por costumbre genérica). Para mix phx.gen.* levantamos app y ejecutamos con docker compose exec app mix ... en vez de container efímero — más rápido en ediciones repetidas y deja iex -S mix disponible.
    • Schema 1/3 — users: mix phx.gen.schema Cuentas.User users rol:string nombre:string email:string pin_hash:string. Edit migration (null: false en rol+nombre, unique_index(:users, [:email]) con nota Postgres NULL≠NULL, index(:users, [:rol])). Edit schema (field :rol, Ecto.Enum, values: [:papa, :hijo], validate_required reducido a [:rol, :nombre], validate_format(:email, ~r/@/) que skipea cuando nil, unique_constraint(:email) que liga al unique_index de BD). mix ecto.migrate limpio.
    • Schema 2/3 — tareas_definicion: mix phx.gen.schema Tareas.Definicion tareas_definicion nombre:string descripcion:text icono:string recurrencia:string dias_semana:array:integer hora_sugerida:time puntos:integer activa:boolean. Edit migration (null: false en nombre+recurrencia, default: 0 y null: false en puntos, default: true en activa — invierto el false que pone el generador). Edit schema (Ecto.Enum, values: [:diaria, :semanal, :una_vez, :manual], defaults sincronizados, validación condicional con validate_dias_semana/1 privado usando get_field + validate_change — patrón Ecto para validaciones condicionales). Punto didáctico clave aterrizado: field :descripcion, :string en schema vs add :descripcion, :text en migration NO es bug — schema = lenguaje Elixir, migration = lenguaje Postgres; Ecto trata :string y :text igual al leer/escribir.
    • Schema 3/3 — tareas_instancia: mix phx.gen.schema Tareas.Instancia tareas_instancia tarea_definicion_id:references:tareas_definicion user_id:references:users fecha:date estado:string completada_at:utc_datetime. El más interesante por FKs + unique compuesto. Edit migration: null: false en FKs+fecha+estado, on_delete: :delete_all (cambia el :nothing del generador), default: "pendiente" en estado, unique_index([:tarea_definicion_id, :user_id, :fecha], name: :tareas_instancia_def_user_fecha_index) con nombre explícito (default Phoenix lo armaría de 61 chars al límite Postgres), index([:user_id, :fecha]) y index([:estado]), eliminé 2 índices solitarios redundantes del generador. Edit schema: cambié field :tarea_definicion_id, :id a belongs_to :tarea_definicion, TareasHijo.Tareas.Definicion (gotcha grande: el generador deja la versión “tonta” sin preload/cast_assoc/assoc_constraint), Ecto.Enum, values: [:pendiente, :hecha], default: :pendiente, assoc_constraint para ambas FKs, unique_constraint con name que coincide EXACTO con el de migration.
    • Bug latente atrapado por revisión cuidadosa del output: la migration creó tarea_instancia_def_user_fecha_index (singular) cuando todos los demás índices del proyecto van plural tareas_instancia_*. Causa: typo en mi snippet (singular en name:). Adicional: en el schema Sergio tipeó :tareas_definicion_id (plural) en cast y validate_required, cuando el FK real (por convención belongs_to) es :tarea_definicion_id singular — Ecto silenciosamente ignora el campo fantasma y validate_required falla con “can’t be blank” para algo que sí pasaste. Fix: mix ecto.rollback + edit migration línea 16 (singular → plural) + edit schema líneas 19-20 (plural → singular) + mix ecto.migrate. Ahora migration y schema coinciden y el unique_constraint capturará violaciones de BD correctamente.
    • Seed (priv/repo/seeds.exs): reemplacé el archivo vacío por seed real. Estructura: delete_all en orden de dependencia (Instancia → Definicion → User para no chocar con FKs), Repo.insert! directo de %User{} y %Definicion{} con bang (script falla ruidoso si algo está mal), ~T[20:00:00] (sigil de Time), dias_semana: [6] para sábado (ISO 1=lun…7=dom), sin pin_hash aún (cast no lo requiere; setting de PIN viene en bloque 4 con Bcrypt). Email papá hardcoded a sevaor@gmail.com. IO.puts al final con confirmación visual de IDs creados.
    • Output del seed: todos los QUERY OK correctos. Detalle didáctico: la última línea (INSERT "Limpiar cuarto") aparece DESPUÉS del IO.puts “Seed listo” — los logs [debug] de Ecto van por proceso Logger (GenServer aparte, C9), IO.puts va directo a stdout, el VM flushea el buffer al terminar. Va a pasar también en LiveView; aterriza la naturaleza asíncrona de procesos. Tipos confirmados en BD: rol: :papa (átomo no string), recurrencia: :diaria, hora_sugerida: ~T[20:00:00], dias_semana: [6].
  • Cosas que Sergio aterrizó del modelo mental (más allá del checklist):
    • belongs_to :foo registra implícitamente field :foo_id, :integer — NO se escriben ambos. Si se duplica, Ecto se queja en compile time.
    • Convención tabla plural ↔ FK singular (tareas_definiciontarea_definicion_id). Es la regla belongs_to :alias → columna alias_id. Si el alias se llama tarea_definicion, la columna es tarea_definicion_id aunque la tabla sea tareas_definicion. Te puede confundir cuando trabajas con tablas que ya son “plural compuesto” como esta.
    • unique_index (BD) + unique_constraint (changeset) con name: que coincide exacto. Sin el segundo, una violación de BD revienta con Postgrex.Error 500 en vez de un error legible de form. Sin coincidencia exacta del nombre, el changeset no captura el error.
    • Índice compuesto (A, B, C) cubre queries WHERE A, WHERE A AND B, WHERE A AND B AND C pero no WHERE B ni WHERE C sin A. Por eso elegí (user_id, fecha) y no al revés — las queries del LiveView del hijo son “mis tareas de hoy” (las dos) y “mis tareas” (solo user); el orden inverso no ayudaría al segundo caso.
    • Repo.insert! (bang) vs Repo.insert (no bang): la convención de “bang = ruidoso” recorre toda la stdlib (Map.fetch vs Map.fetch!). Scripts usan bang; controllers/LiveView no.
    • Inserción directa de %Struct{} sin pasar por changeset está OK en seeds (código mío, errores ruidosos OK) pero prohibida en LiveView/controllers — la frontera de validación siempre es el changeset.
  • Estado de la BD: 3 tablas creadas (schema_migrations + users + tareas_definicion + tareas_instancia), 5 rows insertadas (2 users + 3 definiciones). 0 tareas_instancia aún (las genera el bloque 2 o el cron de Fase 2).
  • Falta de Fase 1 (#184):
    • Bloque 2/4 (próximo): LiveView /hijo con current_user mockeado a Leonardo + lista del día (que genera o consume tareas_instancia del día) + marcar hecho con phx-click → update + PubSub broadcast → vista del papá (mockeada o real) refresca al instante. Aterriza C8 (LiveView lifecycle) y C10 (PubSub) en código real, no en mapa mental.
    • Bloque 3/4: LiveView /admin con CRUD tareas_definicion. Aterriza forms (to_form, phx-change="validate", phx-submit="save").
    • Bloque 4/4: auth real — PIN papá (pin_changeset/2 con Bcrypt.hash_pwd_salt) + PIN hijo (idem) + remembered_devices (4ta migration: device_token uuid, last_seen_at, user_agent, FK user) + sesión via Plug.Conn cookies + LiveView on_mount para inyectar current_user.
  • Commit del proyecto: Sergio commitea localmente con autorización per-sesión (Coach mode: yo no ejecuto git en ~/code/tareas-hijo/). Mensaje sugerido capturado en chat. Push queda a su criterio (este repo NO tiene autorización auto).

2026-05-26 (tarde — #184 bloque 2/4: LiveView /hijo + /papa con PubSub)

  • Pidió: “Vamos a seguir con el bloque 2/4 de #184”. Modo coaching estricto se mantuvo (Sergio escribió 100% del código, corrió 100% de los comandos, yo solo dí snippets + comandos + explicaciones).
  • Plan acordado al inicio (7 pasos): (1) decisión “cómo nacen las tareas_instancia del día”; (2) context TareasHijo.Tareas a mano (sin gen.context); (3) helper de mock user; (4) router con 2 rutas live; (5) HijoLive; (6) PapaLive; (7) probar end-to-end 2 pestañas.
  • Decisión clave (paso 1): Opción A — lazy generation en mount (vs pre-seed o esperar Oban). Razones: funciona sin Oban (que llega hasta Fase 2 #185), aterriza Repo.insert_all/3 y el patrón “read-through con upsert idempotente”, no diluye el bloque con cron. Trade-off aceptado: si nadie abre la app en todo un día, ese día no se materializa — irrelevante para el caso real; Fase 2 lo reemplaza.
  • Flujo de la sesión (Sergio teclea, yo coachéo):
    • Entrega 2.1 (context skeleton + PubSub aislado): lib/tareas_hijo/tareas.ex con @pubsub TareasHijo.PubSub, subscribe/1, topic/1 privado, broadcast/2 privado. Punto didáctico clave aterrizado: C10 regla “context emite, LV escucha” — broadcast privado a propósito para que nadie afuera del context grite al topic. Validación manual en IEx: subscribe(1) + Phoenix.PubSub.broadcast(TareasHijo.PubSub, "tareas:user:1", :hola) + flush():hola. Aterriza visualmente que el proceso IEx recibe el mensaje porque se suscribió al mismo topic. Mismo mecanismo que va a usar el LiveView, solo que con handle_info/2 en vez de flush().
    • Entrega 2.2 (3 funciones de BD): lista_del_dia/2, obtener_o_generar_del_dia/2 (con materializar_dia/2 privado), marcar_hecha/1. Sintaxis from(i in Instancia, where: ..., join: d in assoc(i, :tarea_definicion), order_by: [asc_nulls_last: d.hora_sugerida], preload: [tarea_definicion: d]) aterriza el dialecto declarativo + reuso del join para evitar N+1. materializar_dia/2 filtra definiciones activas por recurrencia == :diaria or (recurrencia == :semanal and ^dia_iso in dias_semana) — punto clave: ^dia_iso in d.dias_semana con array Postgres se compila a $1 = ANY(d.dias_semana), operador nativo eficiente. Repo.insert_all(Instancia, entries, on_conflict: :nothing, conflict_target: [...]) aterrizó: una sola query masiva, no corre changesets ni timestamps automáticos (por eso inserted_at/updated_at explícitos), sí castea Ecto.Enum al pasar el schema (no string). Patrón “let it crash” en :ok = materializar_dia(...) — pattern-match assertion explota en runtime si falla, mejor que devolver lista vacía silenciosa. Gotcha de preloads atrapado: Repo.update descarta preloads → broadcast llevaría tarea_definicion: NotLoaded y el LV crashearía al renderizar → fix: updated = %{updated | tarea_definicion: instancia.tarea_definicion} antes del broadcast. Validación E2E en IEx: 6 pasos (load users → lista_vacía → obtener_o_generar → llamar 2× sin duplicar → subscribe → marcar_hecha → flush con {:tarea_actualizada, ...}).
    • Paso 3 (mock users): lib/tareas_hijo/cuentas.ex mini-context con get_hijo!/0 y get_papa!/0 (4 líneas reales). Punto: ! en nombre + Repo.get_by! siguen la convención “bang = explota si no hay” de la stdlib — y queremos que explote (sin esos users la app no tiene sentido).
    • Paso 4 (stubs HijoLive + PapaLive + router): dos archivos lib/tareas_hijo_web/live/{hijo,papa}_live.ex con mount/3 que asigna current_user (y hijo en el de papá) + render/1 con HEEx mínimo “Hola, X”. Router: live "/hijo", HijoLive + live "/papa", PapaLive. Sergio confirmó que ambas rutas cargaban. Aterriza: el primer contacto con mount 2x (HTTP + WS), use TareasHijoWeb, :live_view invocando __using__(:live_view), @impl true para callbacks, {@current_user.nombre} sintaxis HEEx moderna.
    • Pasos 5+6 (LiveViews enriquecidos): HijoLive con connected?(socket) do Tareas.subscribe(...), lista del día asignada en mount, handle_event("toggle_estado", ...) que solo llama el context y devuelve socket sin tocar (PubSub se encarga del refresh — un solo path de update para todas las pestañas, regla C10 aterrizada en código), handle_info({:tarea_actualizada, instancia}, socket) que reemplaza por id con Enum.map. Render con <li :for={tarea <- @tareas} + class={[..., cond && "..."]} (Tailwind dinámico con nil/false filtrados) + botón con phx-click="toggle_estado" + phx-value-id={tarea.id} + dark mode pareado (regla [[feedback-dark-mode-aesthetics]]). PapaLive casi idéntico pero suscribe al topic del HIJO (no del propio user) + sin handle_event (read-only) + checkbox como <div> decorativo + contador “X de Y hechas” via defp hechas_count/1 privada (paralelos Laravel: helpers locales en el LiveView con defp, no en ViewHelper separado).
    • Bug del end-to-end: Sergio reportó “me aparecen 2 tareas marcadas y no las puedo desmarcar”. Diagnóstico: las 2 marcadas eran residuo del testing IEx de la entrega 2.2 (esperado, quedaron persistidas) + disabled={tarea.estado == :hecha} impedía desmarcar (por diseño de marcar_hecha/1). Fix propuesto y aceptado: refactor a toggle_estado/1 (pendiente ↔ hecha) — un solo verbo, pattern matching case instancia.estado do :pendiente -> %{estado: :hecha, completada_at: ahora}; :hecha -> %{estado: :pendiente, completada_at: nil} end. Cambios: 1 función del context (marcar_hechatoggle_estado), event renombrado "marcar_hecha""toggle_estado", botón sin disabled, cursor-pointer siempre + hover state ambos lados. Aterriza un patrón nuevo (handle_event que decide según estado).
    • Validación E2E final: 3 pestañas abiertas en paralelo (2× /hijo + 1× /papa) — toggle en una, refresh inmediato en las otras dos sin recargar. Suite manual de 7 pasos pasada limpia. Primera vez que Sergio ve PubSub aterrizado en código real tras la sesión teórica del mapa C1-C10 (2026-05-23 noche).
  • Cosas que Sergio aterrizó del modelo mental (más allá del checklist):
    • connected?(socket) guard en mount: efectos colaterales (subscribe, GenServer.cast, etc.) solo en la 2da pasada (WS), no en la 1ra (HTTP). Suscribir en HTTP es leak ligero (proceso muere en 50ms).
    • Diffing automático de HEEx: el broadcast llega a N LiveViews, cada uno corre handle_info, recalcula assigns, Phoenix calcula el diff de HEEx, y solo manda los nodos que cambiaron al browser. Las pestañas NO hablan entre ellas, hablan con el server.
    • class={[list]} con filtrado de nil/false como classnames() built-in — el pattern para clases dinámicas Tailwind.
    • Directivas :for / :if atributos especiales HEEx 1.7+ (vs <%= for ... do %> viejo). Más limpio porque no balancea tags.
    • Repo.update descarta preloads — gotcha que parecía sutil pero hubiera reventado al primer broadcast con Ecto.Association.NotLoaded. Re-pegado manual del preload antes de emitir.
    • from(i in ..., join: d in assoc(i, :rel), preload: [rel: d]) reusa el join para evitar segunda query. Si fuera preload: :rel (sin binding) Ecto haría 2 queries.
  • Estado del proyecto:
    • Fase 1 bloque 2/4 cerrado. 3 archivos nuevos (tareas.ex, cuentas.ex, hijo_live.ex, papa_live.ex) + 1 edit (router.ex).
    • App viva localmente en localhost:4000/hijo y localhost:4000/papa con flujo real funcional.
    • Sigue sin commit (modo coach: Sergio decide cuándo commitear; este repo NO tiene autorización auto).
  • Falta de Fase 1 (#184):
    • Bloque 3/4 (próximo): LiveView /admin con CRUD tareas_definicion. Aterriza forms (to_form, phx-change="validate", phx-submit="save"), changesets en LiveView, modal/inline edit, eliminar con confirmación. Algunos detalles a decidir: ¿una sola página con tabla + form al pie, o /admin/tareas/nueva + /admin/tareas/:id/edit? ¿LiveComponent para el form o todo en el mismo módulo?
    • Bloque 4/4: auth real — PIN papá (pin_changeset/2 con Bcrypt.hash_pwd_salt) + PIN hijo + 4ta migration remembered_devices (device_token uuid, last_seen_at, user_agent, FK user) + sesión via Plug.Conn cookies + on_mount para inyectar current_user (los Cuentas.get_hijo!/get_papa! mock desaparecen). Bcrypt requiere agregar dep :bcrypt_elixir a mix.exs.

2026-05-27 (#184 bloque 3/4: LiveView /admin CRUD + smoke test cross-LV)

  • Pidió: “Quiero avanzar en el proyecto tareas-hijo”. Modo coaching estricto mantenido todo el camino (Sergio escribió 100% del código, corrió 100% de los comandos; yo solo dí snippets + comandos + explicaciones).
  • Estado al inicio: ~/code/tareas-hijo con working tree limpio + 4 commits en main (último c7cd0bf Fase 1 bloque 2/4 ya pusheado a sevaor/tareas-hijo). La bitácora del hub decía “pendiente commit del bloque 2/4” — Sergio lo había committeado desde otra máquina entre sesiones, eliminé la nota obsoleta. Decisiones del bloque 3/4 ya estaban acordadas desde 2026-05-26 (single-page, LiveComponent separado, toggle activa/inactiva, sin broadcast cross-LV, patrón notify_parent, reusar CoreComponents).
  • Flujo (4 entregas, Sergio teclea, yo coachéo):
    • Entrega 3.1 — 5 funciones CRUD del context en lib/tareas_hijo/tareas.ex, nueva sección “Definiciones (CRUD admin)” después de “Mutación”: lista_definiciones/0 (sin filtro activa: true porque admin necesita ver inactivas para reactivar), change_definicion(%Definicion{} = definicion, attrs \\ %{}) (changeset suelto para forms, default %{} permite “form vacío” en :new y “form prellenado” en :edit), crear_definicion(attrs) y actualizar_definicion(%Definicion{}, attrs) (pipeline estándar |> Definicion.changeset(attrs) |> Repo.insert/update, devuelven {:ok, def} o {:error, changeset}), toggle_activa(%Definicion{} = definicion) (atajo con Ecto.Changeset.change(activa: not definicion.activa) que esquiva el validate_required([:nombre, :recurrencia, :puntos, :activa]) del changeset completo). Validación E2E en IEx: 6 pasos pasaron limpios (lista, change vacío, crear, toggle ida-vuelta, actualizar, validación que falla retorna {:error, cs} con errors legibles). Punto didáctico clave aterrizado: Ecto.Changeset.change/2 vs Schema.changeset/2change para cambios quirúrgicos internos (toggle bool de confianza), changeset para entrada del user que cruza la frontera de validación.
    • Entrega 3.2 — DefinicionFormComponent LiveComponent en lib/tareas_hijo_web/live/definicion_form_component.ex (≈140 líneas). use TareasHijoWeb, :live_component (no :live_view). update(%{definicion: definicion} = assigns, socket) deriva changeset y arma form via assign_form/2 privado. handle_event("validate", %{"definicion" => params}, socket) con Map.put(:action, :validate) para forzar errores en form pre-submit. handle_event("save", ...) dispatcha a save_definicion(socket, :new|:edit, params) con dos heads. Form con <.form for={@form} phx-target={@myself} phx-change="validate" phx-submit="save"> + <.input> reusados de CoreComponents para nombre/descripcion/icono/recurrencia (select con prompt)/hora_sugerida (type=time)/puntos (type=number)/activa (type=checkbox). Excepción importante: dias_semana NO usa <.input type="checkbox"> porque el CoreComponent inyecta un hidden value="false" que sirve para boolean pero rompe arrays HTML — escribimos <fieldset> con 7 <input type="checkbox" name="definicion[dias_semana][]" value={num}> a mano. notify_parent(msg) con send(self(), {__MODULE__, msg}) y helper dias_seleccionados/1 normaliza valor del form (puede ser nil, lista de int casteada, o lista de string mientras user teclea). Sanity check mix compile --warnings-as-errors pasó.
    • Entrega 3.3 — AdminLive en lib/tareas_hijo_web/live/admin_live.ex (≈110 líneas) + 1 línea al router (live "/admin", AdminLive). mount/3 asigna current_user (Cuentas.get_papa! mockeado), action: :new, definicion: %Definicion{} y carga definiciones con Tareas.lista_definiciones. handle_event("edit", ...) cambia action: :edit + definicion: <esa> — el LiveComponent embebido se re-monta solo con los nuevos assigns como props (single source of truth para action+definicion en el padre). handle_event("toggle_activa", ...) llama Tareas.toggle_activa + refresca lista + flash. handle_info({DefinicionFormComponent, {:saved, def}}, socket) cierra el ciclo: resetea action a :new, struct vacía, recarga lista, flash. handle_info({DefinicionFormComponent, :cancelado}, socket) resetea sin recargar. Render con tabla HTML cruda (no <.table> del CoreComponent — más control + menos magia) con badges activa/inactiva pareadas dark/light, opacity-50 en filas inactivas, data-confirm="..." en botón “Desactivar”/“Activar” (Phoenix LiveView dispara confirm() del browser automático sin JS). Helpers recurrencia_label/1 (5 heads pattern matching) y dias_label/1 (mapea [1,3,5]"Lun, Mié, Vie").
    • Entrega 3.4 — smoke test cross-LV con 3 pestañas en paralelo (/admin + /hijo + /papa). 5 pasos pasados limpios: (1) baseline confirmando estado inicial; (2) crear def diaria nueva en /admin/hijo y /papa NO cambian (validates “sin broadcast cross-LV de definiciones” — decisión arquitectónica #4); (3) editar nombre de def existente → tabla cambia, /hijo muestra nombre viejo hasta F5 (aterrizado: preload se calcula cada query, pero sin trigger de re-render LiveView no se entera); (4) desactivar def con instancia hoy → fila opaca en /admin, instancia en /hijo sigue ahí (aterrizado: soft-disable preserva histórico que Fase 4 puntos requiere); (5) toggle estado en /hijo refresca /papa al instante (PubSub del bloque 2/4 sigue intacto). Limitación conocida observada: defs nuevas creadas a mitad del día NO entran al “hoy” del hijo porque obtener_o_generar_del_dia/2 solo materializa cuando lista_del_dia está vacía. Fase 2 #185 con Oban cron resuelve.
  • Estado del proyecto:
    • Fase 1 bloque 3/4 cerrado. 2 archivos nuevos LiveView (admin_live.ex, definicion_form_component.ex), 1 edit context (5 funciones nuevas en tareas.ex), 1 edit router (1 línea).
    • App con CRUD admin funcional localmente en localhost:4000/admin + /hijo y /papa heredados del bloque 2/4.
    • Pendiente lado Sergio: commit + push del bloque 3/4 en ~/code/tareas-hijo (Coach mode: yo no commito en el repo del proyecto).
  • Falta de Fase 1 (#184):
    • Bloque 4/4 (siguiente): auth real — agregar dep :bcrypt_elixir a mix.exs; pin_changeset/2 con Bcrypt.hash_pwd_salt en User; 4ta migration remembered_devices (device_token uuid, last_seen_at, user_agent, FK user); páginas de login PIN papá + PIN hijo + lógica “recordar este dispositivo” con cookie firmada; sesión via Plug.Conn; on_mount para inyectar current_user en /hijo, /papa, /admin (retira los Cuentas.get_papa!/get_hijo! mock). Estimación 0.25 sem.

2026-05-27 (tarde — #184 bloque 4/4 EN PROGRESO: sub-entregas 4.1-4.3 ✅, 4.4 compile error)

  • Pidió: “Vamos a avanzar con el bloque 4/4” + a mitad de sesión “Vamos a aligerar un poco el modo coach, quiero que ahora tu crees y modifiques los archivos, pero que me expliques de igual manera los cambios o puntos importantes”. Memoria [[tareas-hijo-learning-mode]] actualizada para reflejar el modo aligerado: yo edito archivos, Sergio corre comandos y commitea, yo sigo explicando.
  • Plan del bloque 4/4 en 4 sub-entregas: 4.1 foundations (deps + migration + schema) · 4.2 context Cuentas con auth · 4.3 SessionController + routes + login HTTP · 4.4 on_mount + retiro de mocks.
  • Sub-entrega 4.1 ✅: agregada dep {:bcrypt_elixir, "~> 3.0"} en mix.exs. Migration 20260527000000_create_remembered_devices.exs con FK a users (on_delete: :delete_all), device_token unique, last_seen_at, user_agent. Schema lib/tareas_hijo/cuentas/remembered_device.ex con belongs_to :user. Schema User extendido con campo virtual :pin, virtual: true, redact: true, split de changesets: changeset/2 (datos del user) vs pin_changeset/2 (cast :pinvalidate_format ~r/^\d{4,6}$/Bcrypt.hash_pwd_saltput_change(:pin_hash, ...)delete_change(:pin) en helper privado hash_pin/1). Seed re-escrito para insertar users via pin_changeset con PIN inicial "1234". Comandos mix deps.get + mix compile + mix ecto.migrate + mix run priv/repo/seeds.exs pasaron limpios.
  • Sub-entrega 4.2 ✅: context Cuentas extendido con 7 funciones: lectura (get_user/1), auth (valid_pin?/2 con Bcrypt.no_user_verify para evitar timing attacks cuando user es nil/sin hash, autenticar_por_rol/2, set_pin/2), remembered devices (recordar_dispositivo/2 con token 32 bytes random URL-safe sin hashear porque 256 bits de entropía no necesitan otra ronda; usuario_por_token/1 refresca last_seen_at en cada hit para cron de limpieza futura; olvidar_dispositivo/1 idempotente). Validación E2E en IEx pasó: PIN correcto/incorrecto, set_pin con error de formato, recordar/buscar/olvidar dispositivo, token inexistente.
  • Sub-entrega 4.3 ✅: 6 archivos. (a) lib/tareas_hijo_web/user_auth.ex nuevo — plug fetch_current_user (lee de Plug.Session o cookie _tareas_hijo_remember y autohidrata sesión desde cookie remember), require_authenticated_user (guarda user_return_to en sesión), require_rol/2 plug-con-arg, log_in_user/3 con renew_session (session fixation mitigada) + put_session(:live_socket_id, "users_sockets:#{user.id}") para futuro logout-en-todos-los-dispositivos + cookie remember 60 días firmada solo si params["recordar"] == "true", log_out_user/1 con broadcast disconnect a sockets vivos + delete cookie + olvidar_dispositivo en BD. (b) SessionController simple (new/create/delete). (c) SessionHTML view module + (d) session_html/new.html.heex con form HTML clásico (pattern="\d{4,6}" + CSRF token explícito + checkbox “recordar” solo si rol=hijo). (e) page_html/home.html.heex reemplazado por landing con 2 cards de rol o “ya iniciaste sesión + ir/cerrar” si hay user. (f) router.ex con plug :fetch_current_user agregado al pipeline :browser + pipelines :require_papa y :require_hijo + rutas get "/login/:rol" + post/delete "/sesion" + scopes que aplican los pipelines a /admin//papa y /hijo. Gotcha didáctico atrapado en vivo: tras agregar :bcrypt_elixir y reiniciar phx.server, el primer POST a /sesion falló con function Bcrypt.verify_pass/2 is undefined (module Bcrypt is not available) — Phoenix code reload recompila módulos pero no arranca aplicaciones nuevas en una VM viva; mix run arranca todas las apps porque es VM fresca. Solución: docker compose restart app. Tras restart: login papá/hijo funcionando, cookie remember escrita en DevTools, auto-login al cerrar/abrir browser validado, rol equivocado bloqueado, logout limpia sesión y cookie. 8 escenarios del plan de prueba pasaron limpios.
  • Sub-entrega 4.4 ⚠️ ESCRITA PERO CON COMPILE ERROR: UserAuth extendido con hooks on_mount(:mount_current_user, ...), (:ensure_papa, ...), (:ensure_hijo, ...) que cargan current_user desde session["user_id"] con assign_new (no sobreescribe si LV padre ya lo puso) + halt+redirect en fallo. Router envolvió los LV en live_session :require_papa y :require_hijo con on_mount: [{UserAuth, :ensure_X}]. Los 3 LV (HijoLive, PapaLive, AdminLive) retiraron Cuentas.get_papa!/0 / get_hijo!/0 para current_user y usan socket.assigns.current_user (inyectado por hook); PapaLive mantiene Cuentas.get_hijo!/0 deliberadamente para localizar al hijo (Fase 1 = 1 hijo; comentario TODO Fase 6 para multi-hijo). Cada LV agregó header con link “Salir” (<.link href={~p"/sesion"} method="delete" data-confirm="...") + navegación cruzada admin↔papa. Compile error: mix compile reportó function redirect/2 imported from both Phoenix.LiveView and Phoenix.Controller, call is ambiguous en user_auth.ex:47 (línea de log_in_user/3). Causa: import Phoenix.LiveView, only: [redirect: 2, put_flash: 3] que agregué para los hooks on_mount choca con el import Phoenix.Controller ya existente del plug. Fix concreto para retomar: quitar el import Phoenix.LiveView, only: [...] completo y usar prefijo Phoenix.LiveView.redirect/2 + Phoenix.LiveView.put_flash/3 solo dentro de los on_mount hooks (∼3 call sites). El plug (log_in_user/3, log_out_user/1, require_authenticated_user/2, require_rol/2) se queda usando Phoenix.Controller.redirect y Phoenix.Controller.put_flash que ya están importados.
  • Estado del repo ~/code/tareas-hijo:
    • Committeado + pusheado: bloques 1-3/4 (último commit con la HEAD de main en origin es el de bloque 3/4 que hizo Sergio al inicio de esta sesión).
    • Working tree con cambios sin commit: TODO el bloque 4/4 (sub-entregas 4.1-4.4). NO commitear hasta arreglar el compile error de 4.4, si no queda el repo en estado roto.
    • Servidor local Phoenix: tras el restart de 4.3 estaba funcionando; tras los cambios de 4.4 con compile error, requiere mix compile exitoso + docker compose restart app antes de retomar pruebas.
  • Falta para cerrar #184 (Fase 1) completa:
    • Aplicar fix del compile error de 4.4 (cambio quirúrgico de import por prefijos completos en ~3 call sites).
    • mix compile limpio + docker compose restart app + mix phx.server.
    • Correr los 8 escenarios del plan de prueba final de 4.4 (incluye logout-en-todos-los-dispositivos vía Endpoint.broadcast("users_sockets:N", "disconnect", %{})).
    • Si pasa todo: commit + push del bloque 4/4 completo en ~/code/tareas-hijo. Modo aligerado: yo no commito en ese repo, Sergio sí.
    • Cerrar Fase 1 #184 completa y actualizar PENDIENTES.md.

2026-05-28 (#184 bloque 4/4 rehecho desde la WSL — Fase 1 COMPLETA)

  • Pidió: “No tuve oportunidad de seguir con tareas-hijo en la casa, ¿se puede seguir aquí?”. Detecté que el repo nunca había estado clonado en esta máquina y que el bloque 4/4 de la PC de casa nunca se pusheó (origin/main estaba en bloque 3/4 3a292fe). Surfaceé la disyuntiva (rehacer aquí vs esperar push de casa vs saltar a Fase 2); Sergio eligió rehacer el 4/4 aquí.
  • Hecho: clonado el repo en ~/code/tareas-hijo (WSL) y reescrito el bloque 4/4 completo desde cero. Diseño funcionalmente equivalente al de la bitácora del 2026-05-27 pero más limpio: en vez de remembered_devices + /login/:rol + /sesion, usé un token de sesión hasheado en users_tokens (estilo phx.gen.auth) + cookie persistente de “recordar dispositivo” (60 días) + login único en /login con 2 botones de rol. Resuelto el compile error que bloqueaba en casa (ambigüedad redirect/2 Phoenix.LiveView vs Phoenix.Controller → prefijo completo en los on_mount). Además / ahora redirige a /login o al home del rol (antes mostraba el welcome de Phoenix).
  • Verificación: mix compile --warnings-as-errors limpio, mix test 5/5 verdes (actualicé page_controller_test por el redirect y config/test.exs para correr tests dockerizados vía DB_HOST), y los 8 escenarios de auth validados E2E con curl en localhost:4000. Detalle completo en Resuelto 2026-05-28 [#184 bloque 4/4 + Fase 1 COMPLETA] arriba.
  • Estado del repo: working tree de la WSL con 9 archivos modificados + 6 nuevos, sin commit (pendiente autorización de Sergio para commit+push). El server dev sigue corriendo en localhost:4000 (docker compose).
  • ⚠️ Divergencia con PC de casa: cuando Sergio retome allá, ese working tree quedó obsoleto — descartar con git fetch && git reset --hard origin/main && git pull antes de tocar nada.
  • Siguiente: #185 (Fase 2 — Oban cron de generación diaria + live_upload con compresión + Tigris + galería de evidencias).

2026-05-29 (#185 Fase 2 — bloques 1/4 + 2/4 ✅)

  • Pidió: “empezar con el pendiente #185” (Fase 2: recurrencia + fotos).
  • Antes de tocar: detecté divergencia en el repo — origin/main ya tenía el 4/4 bueno (user_token, 6ea3575) pero el working tree de la WSL tenía colgando el intento viejo abandonado (remembered_devices, 27-may madrugada, el del compile error). Surfaceé la disyuntiva con un AskUserQuestion; Sergio eligió reset duro a origin + clean. Baseline verde (5 tests) confirmado antes de arrancar.
  • Descompuse #185 en 4 bloques (mismo patrón que Fase 1), ordenados por dependencia externa creciente: 1) Oban+cron+TZ · 2) schema evidencia · 3) Tigris+upload (necesita infra de Sergio) · 4) galería admin.
  • Bloque 1/4 — Oban + generación diaria + TZ Cd. Juárez: deps oban ~> 2.19 (resolvió 2.23) + tzdata ~> 1.1; config Oban (queue default, Pruner 7d, Cron plugin job "0 4 * * *" en America/Ciudad_Juarez); migración add_oban_jobs (schema v14); Oban en el supervisor; Oban testing: :manual en test.exs. En tareas.ex: hoy/0 ahora TZ-aware (DateTime.now!("America/Ciudad_Juarez") — Cd. Juárez observa DST, resolvió MDT −6h), materializar_dia/2 ahora pública y devuelve el count, nueva generar_dia_para_todos_los_hijos/1; la lazy-generation de obtener_o_generar_del_dia/2 se queda como red de seguridad (idempotente vía on_conflict). Cuentas.lista_hijos/0. Worker TareasHijo.Workers.GeneradorDiario. 7 tests nuevos (recurrencia diaria/semanal, exclusión de una_vez/manual/inactivas, idempotencia, generación multi-hijo, worker vía Oban.Testing.perform_job, guard de la config del cron). Prueba de humo: la app arranca con Oban en el árbol y la TZ resuelve.
  • Bloque 2/4 — schema evidencia foto: migración add_evidencia_fotorequiere_foto (bool, default false) en tareas_definicion + evidencia_url (string, nullable) en tareas_instancia; ambos schemas + changesets; checkbox “Requiere foto de evidencia” en el form de admin; badge 📷 en la tabla. 3 tests nuevos. (Los campos de validación abuela y los estados extra en_validacion/aprobada/rechazada son de Fase 3 #186, fuera de scope aquí.)
  • Validación: compile --warnings-as-errors limpio, 15 tests verdes (5 base + 7 bloque 1 + 3 bloque 2), formato OK en archivos nuevos. Modo coach OFF (yo edité todo y corrí todos los comandos).
  • Falta / siguiente:
    • Commit + push de los 2 bloques (pendiente autorización de Sergio; idealmente 2 commits).
    • Bloque 3/4 (Tigris): decisión 2026-05-29 — 1 solo bucket Tigris para dev+prod (con prefijos dev/ y prod/), no MinIO. Bloqueado por acción de Sergio: correr fly storage create -a tareas-hijo (provisiona el bucket vía la extensión Tigris y auto-setea los secrets en la app: AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_ENDPOINT_URL_S3/AWS_REGION/BUCKET_NAME) y copiar esas mismas credenciales a su entorno dev. El código del bloque leerá endpoint/bucket/credenciales de env vars (agnóstico al backend). Sergio setea los secretos; yo no escribo credenciales.
    • Bloque 4/4 (galería admin): después del 3.
  • Deuda de formato: el mix format global de precommit quiere reformatear archivos preexistentes de Fase 1 (admin_live, definicion_form_component, new.html.heex, migración vieja) — herencia de 6ea3575. La dejé fuera de mis commits para no mezclar scope; conviene un “format pass” aparte.

2026-05-29 (#185 Fase 2 — bloques 3/4 + 4/4, Fase 2 COMPLETA ✅)

  • Sergio creó el bucket Tigris (tareas-hijo-evidencias, endpoint https://fly.storage.tigris.dev) con fly storage create, puso las credenciales en ~/code/tareas-hijo/.env (gitignored) y reinició el container. Autorizó commit+push de cada bloque esta sesión.
  • Bloque 3/4 — foto-evidencia a Tigris + compresión client-side: deps ex_aws/ex_aws_s3/sweet_xml/hackney; config ExAws→Tigris (defaults en config.exs, credenciales/endpoint/bucket por env en runtime.exs, agnóstico al backend). Módulo TareasHijo.Almacenamiento (subir_evidencia/3 PUT + url_firmada/1 presigned GET 7d; keys <dev|prod>/evidencias/<yyyy-mm>/<id>-<rand>.jpg). Tareas.adjuntar_evidencia_y_marcar/2. Hook JS ComprimirImagen en app.js (canvas → 1280px → JPEG 0.7 → this.upload). /hijo: tarea con requiere_foto muestra botón 📷 → input con capture=environment (cámara) → compresión → auto_uploadhandle_progress consume → sube a Tigris → marca hecha + miniatura. docker-compose.yml inyecta credenciales dev desde .env; .env añadido a .gitignore. Seed con 2 tareas que piden foto.
  • Bloque 4/4 — galería de evidencias en admin: Tareas.lista_evidencias/1 (instancias con foto, recientes primero, preload definición+hijo, límite). AdminEvidenciasLive en /admin/evidencias (grid de thumbnails con URL firmada precalculada en mount, caption con tarea + hijo + fecha local Cd. Juárez vía DateTime.shift_zone!). Link ”📷 Evidencias” en el header de /admin.
  • Validación E2E real contra Tigris: smoke test put→get(credenciales)→presigned GET(sin credenciales, status 200)→delete, todo OK. Y flujo server completo: materializar_diasubir_evidencia (key con prefijo dev/) → adjuntar_evidencia_y_marcar (estado=hecha + url persistida) → url_firmada (GET 200, bytes coinciden). 17 tests verdes (5 base + 7 bloque 1 + 3 bloque 2 + 2 bloque 4), compile --warnings-as-errors limpio.
  • 4 commits pusheados a origin/main: be4d0da (1/4), 0fbdc11 (2/4), 27f763a (3/4), 9f78637 (4/4). Modo coach OFF: yo escribí todo y corrí todos los comandos.
  • Pendiente menor (verificación de Sergio en navegador): abrir /hijo como hijo (PIN 1234) → tarea “Escritorio ordenado” → 📷 → tomar/elegir foto → confirmar que comprime, sube y aparece la miniatura; luego /admin (PIN 2468) → ”📷 Evidencias” → ver la foto en la galería. Es lo único no validable server-side (canvas client-side + render visual).
  • Siguiente: #186 (Fase 3 — validación abuela vía deep link WhatsApp), ya desbloqueada.

2026-05-29 (#185 — verificación en navegador: bug del upload + fix, miniatura en /papa)

  • Sergio probó en el navegador y el upload no arrancaba: seleccionaba la foto y no pasaba nada, sin error en consola. Se instrumentó el hook con console.log: confirmó que monta, dispara change, comprime (71 KB→38 KB) y llama this.upload sin error — pero la subida nunca empezaba.
  • Causa raíz (verificada en el fuente de phoenix_live_view.js): this.upload(name, files)dispatchUploads busca un input con data-phx-upload-ref y name coincidente (solo lo da live_file_input), le trackea el archivo y dispara un evento "input" sintético. Pero LiveView solo arranca el preflight (auto_upload) si ese input pertenece a un <form> con phx-change (el código usa inputEl.form / uploadFiles(inputEl.form, …)). El input plano suelto del bloque 3 no tenía ni data-phx-upload-ref ni form → trackeaba pero nunca subía, sin error.
  • Fix (commit 1e74a18): en /hijo, el live_file_input (ancla, oculto) ahora va dentro de <form phx-change="validar_evidencia"> (handler noop). El input visible + hook de compresión quedan igual. Subida confirmada en navegador por Sergio (miniatura visible). Aprendizaje guardado en memoria [[reference-liveview-thisupload-needs-form]].
  • Mejora /papa: la galería completa vive en /admin/evidencias, pero Sergio esperaba ver las fotos en /papa (su vista de seguimiento). Se agregó la miniatura de evidencia (clic → foto completa) + badge 📷 en las tareas que piden foto, en cada tarea hecha de /papa.
  • Estado: Fase 2 (#185) verificada E2E en navegador. origin/main en 1e74a18. 17 tests verdes.