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_uploadpara 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_elixircon 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-mailervía container efímero, dev dockerizado (Dockerfile.dev + docker-compose.yml + Postgres 16-alpine), repo privadosevaor/tareas-hijo, deploy a Fly.io endfw(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 customtareas.sevaor.devpendiente — 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_uploadcon 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/1es 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/mainend5db33d= 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_transaccionesyadd_puntos_otorgados_to_instanciaen Fly al hacerfly 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:
- Madrugada-tarde: coach estricto (yo snippet, Sergio teclea). Cerraron bloques 0-3/4 Fase 1.
- Tarde (bloque 4/4): coach aligerado — yo edito archivos, Sergio corre comandos y commitea.
- Noche (2026-05-27): Sergio pidió deshabilitar el modo coach completo. A partir de ahora
tareas-hijose 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_at — gancho 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/1 → toggle_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_uploadcon preview/progress/drag-and-drop sin JS.- Hot reload instantáneo en dev.
mix phx.gen.authgenera 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, nonull, 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
mainapuntando al objeto corrupto. Working tree intacto. Recovery: backup defensivo de.git,update-refal último commit bueno (19141cac), borrar 4 loose vacíos, limpiar reflogs/cache-tree corruptos, rebuild de.git/index. Después:git ls-remotereveló 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 dememory/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
usersrow — vive en tablavalidadoresseparada; 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 ensolicitado— evita “puntos en limbo” si papá deniega canje.
- No
- 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 repotareas_hijo(snake_case Elixir) y del subdominio tentativotareas.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.mdreescrito completo (paso 2-5 reemplazados). Sección “Integraciones externas” cambió “Meta Cloud API” por “WhatsApp vía deep linkwa.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.exscomo 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.servercomo reemplazo de tinker + servidor en vivo,mix releasepara Fly.io, aliases como scripts.json,mix.lock, configuración por entorno con la trampaprod.exsvsruntime.exs(vars de entorno deben ir enruntime.exso 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/vslib/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.exspara env vars en prod, noprod.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 conTareasHijo.*. 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.newdeja, splitlib/app/(negocio) vslib/app_web/(HTTP), convención naming módulo↔archivo, contexts como API entre_weby BD, diagrama de flujo de request, archivos top-levelapp.ex+app_web.excon__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 (castwhitelist +validate_*+unique_constraint); Repo como única puerta; Query DSL con macros y pinning^var; preload explícito; transacciones conMulti; constraints vs validations; embedded schemas; ejemplo realista completo deInstanciacon 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 conto_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/2como raíz, child specs, 3 estrategias de restart (one_for_one/one_for_all/rest_for_one),max_restarts/max_secondscomo red de seguridad, GenServer básico con call/cast, DynamicSupervisor, encaje de Phoenix en el árbol, panorama completo del árbol de tareas-hijo, cuándotry/rescueSÍ 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_infocomo 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).
- C6 — Estructura Phoenix: árbol que
- 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 (tentativotareas.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 fetchal iniciar sesión hub +git pull --ff-onlyantes 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 condocker 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.exshostnamelocalhost→db. - Rename
master→main, README dev local sin referencia a Elixir/Postgres en host, commit inicial9e7bd1d. - GitHub:
apt install gh(repo oficial) +gh auth loginSSH +gh repo create tareas-hijo --private --source . --remote origin --push. - Fly:
curl https://fly.io/install.sh | sh+ auth → apptareas-hijoen org personal + 3 secrets staged + Postgres unmanagedtareas-hijo-dbendfw(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.
- Setup local:
- 3 gotchas que costaron tiempo, capturadas en memoria [[phoenix-fly-setup-gotchas]]:
- 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. Aunquedown -vborre 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 directorycryptic. Fix:sudo rm -rf deps _build+ bind mount puro (en WSL2 con código en filesystem Linux tiene performance nativa y bonus de IDE vedeps/para ElixirLS). flyctl launchcompila 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 conmix phx.gen.release --dockerdentro del container + fly.toml a mano +flyctl apps createsin scanner + secrets + postgres create/attach + deploy.- Phoenix 1.8 colocated hooks — esbuild en
mix assets.deployfalla conCould not resolve "phoenix-colocated/tareas_hijo"simix compileno corrió antes. El compiler:phoenix_live_viewgenera el dir_build/.../phoenix-colocated/<app>/durante compile, y esbuild lo necesita. Mi Dockerfile inicial teníaassets.deployantes decompile(template Phoenix 1.7 viejo); invertir solucionó.
- Docker compose named volumes anidados en bind mounts → al primer arranque con
- Bonus gotcha:
mix phx.gen.release --dockerconsulta 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 oficialelixir: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>ensevaor/tareas-hijocon 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.devqueda como tarea menor (no bloquea Fase 1; configurable después conflyctl 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_subscriptionyavatar_url(Fase 5 push),evidencia_urlyrequiere_foto(Fase 2 fotos),requiere_validacionyvalidador_id(Fase 3 validación abuela),puntos_otorgados(Fase 4 puntos). Patrón: cada fase agrega sus columnas conmix 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ísicolib/tareas_hijo/cuentas/,lib/tareas_hijo/tareas/. - Generador
mix phx.gen.schema(nogen.context).gen.schemagenera 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.Enumen schema (noCREATE 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 -dparadb(el servicio endocker-compose.ymlse llamaapp, noweb— corregido en marcha; uséwebinicial por costumbre genérica). Paramix phx.gen.*levantamosappy ejecutamos condocker compose exec app mix ...en vez de container efímero — más rápido en ediciones repetidas y dejaiex -S mixdisponible. - Schema 1/3 —
users:mix phx.gen.schema Cuentas.User users rol:string nombre:string email:string pin_hash:string. Edit migration (null: falseen rol+nombre,unique_index(:users, [:email])con nota Postgres NULL≠NULL,index(:users, [:rol])). Edit schema (field :rol, Ecto.Enum, values: [:papa, :hijo],validate_requiredreducido a[:rol, :nombre],validate_format(:email, ~r/@/)que skipea cuando nil,unique_constraint(:email)que liga al unique_index de BD).mix ecto.migratelimpio. - 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: falseen nombre+recurrencia,default: 0ynull: falseen puntos,default: trueen activa — invierto elfalseque pone el generador). Edit schema (Ecto.Enum, values: [:diaria, :semanal, :una_vez, :manual], defaults sincronizados, validación condicional convalidate_dias_semana/1privado usandoget_field+validate_change— patrón Ecto para validaciones condicionales). Punto didáctico clave aterrizado:field :descripcion, :stringen schema vsadd :descripcion, :texten migration NO es bug — schema = lenguaje Elixir, migration = lenguaje Postgres; Ecto trata:stringy:textigual 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: falseen FKs+fecha+estado,on_delete: :delete_all(cambia el:nothingdel 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])yindex([:estado]), eliminé 2 índices solitarios redundantes del generador. Edit schema: cambiéfield :tarea_definicion_id, :idabelongs_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_constraintpara ambas FKs,unique_constraintcon 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 pluraltareas_instancia_*. Causa: typo en mi snippet (singular enname:). Adicional: en el schema Sergio tipeó:tareas_definicion_id(plural) encastyvalidate_required, cuando el FK real (por convenciónbelongs_to) es:tarea_definicion_idsingular — Ecto silenciosamente ignora el campo fantasma yvalidate_requiredfalla 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 elunique_constraintcapturará violaciones de BD correctamente. - Seed (
priv/repo/seeds.exs): reemplacé el archivo vacío por seed real. Estructura:delete_allen 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), sinpin_hashaún (cast no lo requiere; setting de PIN viene en bloque 4 conBcrypt). Email papá hardcoded asevaor@gmail.com.IO.putsal final con confirmación visual de IDs creados. - Output del seed: todos los
QUERY OKcorrectos. Detalle didáctico: la última línea (INSERT "Limpiar cuarto") aparece DESPUÉS delIO.puts“Seed listo” — los logs[debug]de Ecto van por procesoLogger(GenServer aparte, C9),IO.putsva 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].
- Setup:
- Cosas que Sergio aterrizó del modelo mental (más allá del checklist):
belongs_to :fooregistra implícitamentefield :foo_id, :integer— NO se escriben ambos. Si se duplica, Ecto se queja en compile time.- Convención tabla plural ↔ FK singular (
tareas_definicion→tarea_definicion_id). Es la reglabelongs_to :alias→ columnaalias_id. Si el alias se llamatarea_definicion, la columna estarea_definicion_idaunque la tabla seatareas_definicion. Te puede confundir cuando trabajas con tablas que ya son “plural compuesto” como esta. unique_index(BD) +unique_constraint(changeset) conname:que coincide exacto. Sin el segundo, una violación de BD revienta conPostgrex.Error500 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 queriesWHERE A,WHERE A AND B,WHERE A AND B AND Cpero noWHERE BniWHERE Csin 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) vsRepo.insert(no bang): la convención de “bang = ruidoso” recorre toda la stdlib (Map.fetchvsMap.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). 0tareas_instanciaaún (las genera el bloque 2 o el cron de Fase 2). - Falta de Fase 1 (#184):
- Bloque 2/4 (próximo): LiveView
/hijoconcurrent_usermockeado a Leonardo + lista del día (que genera o consumetareas_instanciadel día) + marcar hecho conphx-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
/admincon CRUDtareas_definicion. Aterriza forms (to_form,phx-change="validate",phx-submit="save"). - Bloque 4/4: auth real — PIN papá (
pin_changeset/2conBcrypt.hash_pwd_salt) + PIN hijo (idem) +remembered_devices(4ta migration:device_token uuid,last_seen_at,user_agent, FK user) + sesión viaPlug.Conncookies + LiveViewon_mountpara inyectarcurrent_user.
- Bloque 2/4 (próximo): LiveView
- Commit del proyecto: Sergio commitea localmente con autorización per-sesión (Coach mode: yo no ejecuto
giten~/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_instanciadel día”; (2) contextTareasHijo.Tareasa mano (singen.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), aterrizaRepo.insert_all/3y 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.excon@pubsub TareasHijo.PubSub,subscribe/1,topic/1privado,broadcast/2privado. 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 conhandle_info/2en vez deflush(). - Entrega 2.2 (3 funciones de BD):
lista_del_dia/2,obtener_o_generar_del_dia/2(conmaterializar_dia/2privado),marcar_hecha/1. Sintaxisfrom(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/2filtra definiciones activas porrecurrencia == :diaria or (recurrencia == :semanal and ^dia_iso in dias_semana)— punto clave:^dia_iso in d.dias_semanacon 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 esoinserted_at/updated_atexplícitos), sí casteaEcto.Enumal 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.updatedescarta preloads → broadcast llevaríatarea_definicion: NotLoadedy 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.exmini-context conget_hijo!/0yget_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.exconmount/3que asignacurrent_user(yhijoen el de papá) +render/1con HEEx mínimo “Hola, X”. Router:live "/hijo", HijoLive+live "/papa", PapaLive. Sergio confirmó que ambas rutas cargaban. Aterriza: el primer contacto conmount2x (HTTP + WS),use TareasHijoWeb, :live_viewinvocando__using__(:live_view),@impl truepara callbacks,{@current_user.nombre}sintaxis HEEx moderna. - Pasos 5+6 (LiveViews enriquecidos):
HijoLiveconconnected?(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 conEnum.map. Render con<li :for={tarea <- @tareas}+class={[..., cond && "..."]}(Tailwind dinámico connil/falsefiltrados) + botón conphx-click="toggle_estado"+phx-value-id={tarea.id}+ dark mode pareado (regla [[feedback-dark-mode-aesthetics]]).PapaLivecasi idéntico pero suscribe al topic del HIJO (no del propio user) + sinhandle_event(read-only) + checkbox como<div>decorativo + contador “X de Y hechas” viadefp hechas_count/1privada (paralelos Laravel: helpers locales en el LiveView condefp, 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 demarcar_hecha/1). Fix propuesto y aceptado: refactor atoggle_estado/1(pendiente ↔ hecha) — un solo verbo, pattern matchingcase instancia.estado do :pendiente -> %{estado: :hecha, completada_at: ahora}; :hecha -> %{estado: :pendiente, completada_at: nil} end. Cambios: 1 función del context (marcar_hecha→toggle_estado), event renombrado"marcar_hecha"→"toggle_estado", botón sindisabled,cursor-pointersiempre + 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).
- Entrega 2.1 (context skeleton + PubSub aislado):
- 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 denil/falsecomoclassnames()built-in — el pattern para clases dinámicas Tailwind.- Directivas
:for/:ifatributos especiales HEEx 1.7+ (vs<%= for ... do %>viejo). Más limpio porque no balancea tags. Repo.updatedescarta preloads — gotcha que parecía sutil pero hubiera reventado al primer broadcast conEcto.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 fuerapreload: :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/hijoylocalhost:4000/papacon flujo real funcional. - Sigue sin commit (modo coach: Sergio decide cuándo commitear; este repo NO tiene autorización auto).
- Fase 1 bloque 2/4 cerrado. 3 archivos nuevos (
- Falta de Fase 1 (#184):
- Bloque 3/4 (próximo): LiveView
/admincon CRUDtareas_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/2conBcrypt.hash_pwd_salt) + PIN hijo + 4ta migrationremembered_devices(device_token uuid,last_seen_at,user_agent, FK user) + sesión viaPlug.Conncookies +on_mountpara inyectarcurrent_user(losCuentas.get_hijo!/get_papa!mock desaparecen). Bcrypt requiere agregar dep:bcrypt_elixiramix.exs.
- Bloque 3/4 (próximo): LiveView
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-hijocon working tree limpio + 4 commits enmain(últimoc7cd0bfFase 1 bloque 2/4 ya pusheado asevaor/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ónnotify_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 filtroactiva: trueporque admin necesita ver inactivas para reactivar),change_definicion(%Definicion{} = definicion, attrs \\ %{})(changeset suelto para forms, default%{}permite “form vacío” en:newy “form prellenado” en:edit),crear_definicion(attrs)yactualizar_definicion(%Definicion{}, attrs)(pipeline estándar|> Definicion.changeset(attrs) |> Repo.insert/update, devuelven{:ok, def}o{:error, changeset}),toggle_activa(%Definicion{} = definicion)(atajo conEcto.Changeset.change(activa: not definicion.activa)que esquiva elvalidate_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/2vsSchema.changeset/2—changepara cambios quirúrgicos internos (toggle bool de confianza),changesetpara entrada del user que cruza la frontera de validación. - Entrega 3.2 —
DefinicionFormComponentLiveComponent enlib/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 viaassign_form/2privado.handle_event("validate", %{"definicion" => params}, socket)conMap.put(:action, :validate)para forzar errores en form pre-submit.handle_event("save", ...)dispatcha asave_definicion(socket, :new|:edit, params)con dos heads. Form con<.form for={@form} phx-target={@myself} phx-change="validate" phx-submit="save">+<.input>reusados deCoreComponentspara nombre/descripcion/icono/recurrencia (select con prompt)/hora_sugerida (type=time)/puntos (type=number)/activa (type=checkbox). Excepción importante:dias_semanaNO usa<.input type="checkbox">porque el CoreComponent inyecta un hiddenvalue="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)consend(self(), {__MODULE__, msg})y helperdias_seleccionados/1normaliza valor del form (puede ser nil, lista de int casteada, o lista de string mientras user teclea). Sanity checkmix compile --warnings-as-errorspasó. - Entrega 3.3 —
AdminLiveenlib/tareas_hijo_web/live/admin_live.ex(≈110 líneas) + 1 línea al router (live "/admin", AdminLive).mount/3asignacurrent_user(Cuentas.get_papa!mockeado),action: :new,definicion: %Definicion{}y cargadefinicionesconTareas.lista_definiciones.handle_event("edit", ...)cambiaaction: :edit+definicion: <esa>— el LiveComponent embebido se re-monta solo con los nuevos assigns como props (single source of truth paraaction+definicionen el padre).handle_event("toggle_activa", ...)llamaTareas.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-50en filas inactivas,data-confirm="..."en botón “Desactivar”/“Activar” (Phoenix LiveView disparaconfirm()del browser automático sin JS). Helpersrecurrencia_label/1(5 heads pattern matching) ydias_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→/hijoy/papaNO cambian (validates “sin broadcast cross-LV de definiciones” — decisión arquitectónica #4); (3) editar nombre de def existente → tabla cambia,/hijomuestra 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/hijosigue ahí (aterrizado: soft-disable preserva histórico que Fase 4 puntos requiere); (5) toggle estado en/hijorefresca/papaal 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 porqueobtener_o_generar_del_dia/2solo materializa cuandolista_del_diaestá vacía. Fase 2 #185 con Oban cron resuelve.
- Entrega 3.1 — 5 funciones CRUD del context en
- 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 entareas.ex), 1 edit router (1 línea). - App con CRUD admin funcional localmente en
localhost:4000/admin+/hijoy/papaheredados 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).
- Fase 1 bloque 3/4 cerrado. 2 archivos nuevos LiveView (
- Falta de Fase 1 (#184):
- Bloque 4/4 (siguiente): auth real — agregar dep
:bcrypt_elixiramix.exs;pin_changeset/2conBcrypt.hash_pwd_saltenUser; 4ta migrationremembered_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 viaPlug.Conn;on_mountpara inyectarcurrent_useren/hijo,/papa,/admin(retira losCuentas.get_papa!/get_hijo!mock). Estimación 0.25 sem.
- Bloque 4/4 (siguiente): auth real — agregar dep
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"}enmix.exs. Migration20260527000000_create_remembered_devices.exscon FK ausers(on_delete: :delete_all),device_tokenunique,last_seen_at,user_agent. Schemalib/tareas_hijo/cuentas/remembered_device.exconbelongs_to :user. SchemaUserextendido con campo virtual:pin, virtual: true, redact: true, split de changesets:changeset/2(datos del user) vspin_changeset/2(cast:pin→validate_format ~r/^\d{4,6}$/→Bcrypt.hash_pwd_salt→put_change(:pin_hash, ...)→delete_change(:pin)en helper privadohash_pin/1). Seed re-escrito para insertar users viapin_changesetcon PIN inicial"1234". Comandosmix deps.get+mix compile+mix ecto.migrate+mix run priv/repo/seeds.exspasaron limpios. - Sub-entrega 4.2 ✅: context
Cuentasextendido con 7 funciones: lectura (get_user/1), auth (valid_pin?/2conBcrypt.no_user_verifypara evitar timing attacks cuando user es nil/sin hash,autenticar_por_rol/2,set_pin/2), remembered devices (recordar_dispositivo/2con token 32 bytes random URL-safe sin hashear porque 256 bits de entropía no necesitan otra ronda;usuario_por_token/1refrescalast_seen_aten cada hit para cron de limpieza futura;olvidar_dispositivo/1idempotente). 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.exnuevo — plugfetch_current_user(lee de Plug.Session o cookie_tareas_hijo_remembery autohidrata sesión desde cookie remember),require_authenticated_user(guardauser_return_toen sesión),require_rol/2plug-con-arg,log_in_user/3conrenew_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 siparams["recordar"] == "true",log_out_user/1con broadcast disconnect a sockets vivos + delete cookie +olvidar_dispositivoen BD. (b)SessionControllersimple (new/create/delete). (c)SessionHTMLview module + (d)session_html/new.html.heexcon form HTML clásico (pattern="\d{4,6}"+ CSRF token explícito + checkbox “recordar” solo si rol=hijo). (e)page_html/home.html.heexreemplazado por landing con 2 cards de rol o “ya iniciaste sesión + ir/cerrar” si hay user. (f)router.excon plug:fetch_current_useragregado al pipeline:browser+ pipelines:require_papay:require_hijo+ rutasget "/login/:rol"+post/delete "/sesion"+ scopes que aplican los pipelines a/admin//papay/hijo. Gotcha didáctico atrapado en vivo: tras agregar:bcrypt_elixiry reiniciarphx.server, el primer POST a/sesionfalló confunction 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 runarranca 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:
UserAuthextendido con hookson_mount(:mount_current_user, ...),(:ensure_papa, ...),(:ensure_hijo, ...)que cargancurrent_userdesdesession["user_id"]conassign_new(no sobreescribe si LV padre ya lo puso) + halt+redirect en fallo. Router envolvió los LV enlive_session :require_papay:require_hijoconon_mount: [{UserAuth, :ensure_X}]. Los 3 LV (HijoLive,PapaLive,AdminLive) retiraronCuentas.get_papa!/0/get_hijo!/0paracurrent_usery usansocket.assigns.current_user(inyectado por hook);PapaLivemantieneCuentas.get_hijo!/0deliberadamente 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 compilereportófunction redirect/2 imported from both Phoenix.LiveView and Phoenix.Controller, call is ambiguousenuser_auth.ex:47(línea delog_in_user/3). Causa:import Phoenix.LiveView, only: [redirect: 2, put_flash: 3]que agregué para los hookson_mountchoca con elimport Phoenix.Controllerya existente del plug. Fix concreto para retomar: quitar elimport Phoenix.LiveView, only: [...]completo y usar prefijoPhoenix.LiveView.redirect/2+Phoenix.LiveView.put_flash/3solo dentro de loson_mounthooks (∼3 call sites). El plug (log_in_user/3,log_out_user/1,require_authenticated_user/2,require_rol/2) se queda usandoPhoenix.Controller.redirectyPhoenix.Controller.put_flashque ya están importados. - Estado del repo
~/code/tareas-hijo:- Committeado + pusheado: bloques 1-3/4 (último commit con la HEAD de
mainen 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 compileexitoso +docker compose restart appantes de retomar pruebas.
- Committeado + pusheado: bloques 1-3/4 (último commit con la HEAD de
- Falta para cerrar #184 (Fase 1) completa:
- Aplicar fix del compile error de 4.4 (cambio quirúrgico de
importpor prefijos completos en ~3 call sites). mix compilelimpio +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.
- Aplicar fix del compile error de 4.4 (cambio quirúrgico de
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 deremembered_devices+/login/:rol+/sesion, usé un token de sesión hasheado enusers_tokens(estilo phx.gen.auth) + cookie persistente de “recordar dispositivo” (60 días) + login único en/logincon 2 botones de rol. Resuelto el compile error que bloqueaba en casa (ambigüedadredirect/2Phoenix.LiveView vs Phoenix.Controller → prefijo completo en loson_mount). Además/ahora redirige a/logino al home del rol (antes mostraba el welcome de Phoenix). - Verificación:
mix compile --warnings-as-errorslimpio,mix test5/5 verdes (actualicépage_controller_testpor el redirect yconfig/test.exspara correr tests dockerizados víaDB_HOST), y los 8 escenarios de auth validados E2E con curl enlocalhost: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 pullantes de tocar nada. - Siguiente: #185 (Fase 2 — Oban cron de generación diaria +
live_uploadcon 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/mainya 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 (queuedefault, Pruner 7d, Cron plugin job"0 4 * * *"enAmerica/Ciudad_Juarez); migraciónadd_oban_jobs(schema v14); Oban en el supervisor;Oban testing: :manualentest.exs. Entareas.ex:hoy/0ahora TZ-aware (DateTime.now!("America/Ciudad_Juarez")— Cd. Juárez observa DST, resolvió MDT −6h),materializar_dia/2ahora pública y devuelve el count, nuevagenerar_dia_para_todos_los_hijos/1; la lazy-generation deobtener_o_generar_del_dia/2se queda como red de seguridad (idempotente víaon_conflict).Cuentas.lista_hijos/0. WorkerTareasHijo.Workers.GeneradorDiario. 7 tests nuevos (recurrencia diaria/semanal, exclusión deuna_vez/manual/inactivas, idempotencia, generación multi-hijo, worker víaOban.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_foto→requiere_foto(bool, default false) entareas_definicion+evidencia_url(string, nullable) entareas_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 extraen_validacion/aprobada/rechazadason de Fase 3 #186, fuera de scope aquí.) - Validación: compile
--warnings-as-errorslimpio, 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/yprod/), no MinIO. Bloqueado por acción de Sergio: correrfly 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 formatglobal deprecommitquiere reformatear archivos preexistentes de Fase 1 (admin_live,definicion_form_component,new.html.heex, migración vieja) — herencia de6ea3575. 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, endpointhttps://fly.storage.tigris.dev) confly 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 enconfig.exs, credenciales/endpoint/bucket por env enruntime.exs, agnóstico al backend). MóduloTareasHijo.Almacenamiento(subir_evidencia/3PUT +url_firmada/1presigned GET 7d; keys<dev|prod>/evidencias/<yyyy-mm>/<id>-<rand>.jpg).Tareas.adjuntar_evidencia_y_marcar/2. Hook JSComprimirImagenenapp.js(canvas → 1280px → JPEG 0.7 →this.upload)./hijo: tarea conrequiere_fotomuestra botón 📷 → input concapture=environment(cámara) → compresión →auto_upload→handle_progressconsume → sube a Tigris → marca hecha + miniatura.docker-compose.ymlinyecta credenciales dev desde.env;.envañ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).AdminEvidenciasLiveen/admin/evidencias(grid de thumbnails con URL firmada precalculada enmount, caption con tarea + hijo + fecha local Cd. Juárez víaDateTime.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_dia→subir_evidencia(key con prefijodev/) →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-errorslimpio. - 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
/hijocomo 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, disparachange, comprime (71 KB→38 KB) y llamathis.uploadsin error — pero la subida nunca empezaba. - Causa raíz (verificada en el fuente de
phoenix_live_view.js):this.upload(name, files)→dispatchUploadsbusca un input condata-phx-upload-refynamecoincidente (solo lo dalive_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>conphx-change(el código usainputEl.form/uploadFiles(inputEl.form, …)). El input plano suelto del bloque 3 no tenía nidata-phx-upload-refni form → trackeaba pero nunca subía, sin error. - Fix (commit
1e74a18): en/hijo, ellive_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/mainen1e74a18. 17 tests verdes.