Commit Graph

16 Commits

Author SHA1 Message Date
alexandrev-tibco 4579b9bb69 watch: enviar la semana aunque todavia no este planificada
Faltaba la otra mitad del "no se actualiza": makePayload devolvia nil cuando la
semana en curso no tenia plan creado, asi que el iPhone no mandaba nada — ni
respondia cuando el reloj pedia datos — y el reloj se quedaba con el ultimo
snapshot, el del miercoles. Ahora la semana viaja igualmente, vacia, y el reloj
dice que no hay nada planificado en vez de enseñar lo de hace dias.

Ademas el reloj muestra cuando se actualizo por ultima vez. Sin eso, un
snapshot viejo y uno recien llegado se ven igual, y no hay forma de saber si
el problema es el envio o lo que se pinta.

Refs #34, #35

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 16:32:34 +02:00
alexandrev-tibco a049e4f74b watch: el dia de hoy se resuelve al pintar, no al generar el envio
El iPhone marcaba isToday al construir el snapshot y el reloj se creia esa
marca para siempre. Como el snapshot sobrevive en el app group, un sabado
seguia enseñando la cena del miercoles: el dia que era cuando se genero.

Ahora cada dia viaja con su fecha real y el reloj busca hoy cuando dibuja. Si
el snapshot es de otra semana no hay "hoy" que enseñar, asi que en vez de
colar la comida de otro dia lo dice y pide abrir el iPhone. Para snapshots
guardados por versiones anteriores, que no traen fecha, solo se confia en el
indice del dia mientras el propio snapshot sea de esta semana.

Ademas el reloj pide datos al abrirse y el iPhone, en vez de responder con lo
ultimo que cacheo, reconstruye la semana desde el store — que es la otra mitad
del "no se actualiza": el cache podia ser de hace dias.

Y cuando no hay nada planificado se dice, que antes era un guion indistinguible
de una comida marcada como no planificada: "Hoy no hay nada planificado" para
el dia entero y "Sin planificar" por comida, en los 6 idiomas (el bundle del
reloj no tiene cadenas propias, viajan en el envio).

Refs #34, #35

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 13:21:21 +02:00
alexandrev-tibco d8345a5b20 Revert "hogar compartido: colaboracion entre cuentas sobre Firestore"
El hogar compartido cambia donde viven los datos, asi que se va a la 3.0 y la
2.x queda para correcciones. El trabajo sigue vivo en la rama 3.0.0 (163fd60).

Importante para esta rama: esto tambien retira el entitlement
com.apple.developer.applesignin, que sin la capability marcada en el App ID
habria roto la firma al subir la 2.1.1 a TestFlight.

This reverts commit 163fd60.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 13:08:27 +02:00
alexandrev-tibco 163fd6026a hogar compartido: colaboracion entre cuentas sobre Firestore
CloudKit sincroniza la base privada de un Apple ID: ni llega a Android ni deja
que dos cuentas editen el mismo plan (CKShare sigue sin existir en SwiftData).
El contenido de un hogar pasa por tanto a Firestore, y un dispositivo que entra
en un hogar construye el store local sin CloudKit — dos espejos escribiendo los
mismos objetos se pelean, que es justo lo que ya obligó a apagar el sync por
iCloud KV.

SwiftData sigue siendo el store local y el modo offline; HouseholdSyncService es
lo unico que habla con la red. Detecta cambios comparando una huella del
contenido de cada documento con la ultima sincronizada (el "shadow"), asi que no
hace falta instrumentar con updatedAt las treinta vistas que mutan modelos. Los
borrados van como tombstone: un borrado duro volveria desde cualquier miembro
que estuviera sin conexion.

Semanas y slots usan id derivado del contenido (2026-09-14, 5-dinner) para que
dos miembros que abren la misma semana escriban el mismo documento en vez de
crear dos, y para que los conflictos se resuelvan por slot y no por semana.

Incluye reglas de seguridad (solo miembros; los codigos de invitacion se pueden
leer por id pero no listar), pantalla de hogar en Ajustes con Sign in with Apple,
invitacion por codigo de 6 caracteres sin vocales ni 0/O/1/I, y la eleccion al
unirse entre llevarse los platos propios o adoptar los del hogar.

Fuera de esta fase: fotos de platos (necesitan Storage) y el cliente Android.

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 13:00:38 +02:00
alexandrev-tibco 66128e7a5c premium: configurar días y comidas de una semana concreta
Hasta ahora includeWeekends y activeMealTypes vivían solo en AppSettings y
aplicaban a todo el calendario. WeekPlan puede ahora sobreescribirlos para su
semana (includeWeekendsOverride / mealTypesOverrideRaw) y WeekSchedule resuelve
qué manda en cada semana: su override si lo tiene, los ajustes globales si no.

Todo lo que pintaba días y comidas —planificador, exports, compartir texto,
widget, watch— pasa por settings.schedule(for:), así que una semana con fin de
semana añadido o con desayuno activo se ve igual en todas partes.

Crear o editar el override es premium; los ya guardados se respetan aunque la
suscripción caduque, para no borrar comidas ya planificadas. La hoja avisa y
pide confirmación cuando quitar un día o una comida se lleva por delante algo
ya planificado.

Refs #32

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 12:05:34 +02:00
alexandrev-tibco 24a6743ed2 reglas: los avisos se calculan del plan actual, no de una marca guardada
Un plato aparecia en la lista de incumplimientos sin explicacion (captura
del usuario: "Ensalada campera"): findViolations listaba los slots con
isRuleOverridden, una marca puesta al asignar que quedaba obsoleta cuando
el conflicto ya se habia resuelto. Y al reves, editar una regla despues
de planificar no detectaba nada.

- findViolations recorre el plan y reporta solo lo que incumple AHORA,
  siempre con sus razones (nunca una entrada vacia)
- MealSlot.isRuleIgnored: "Ignorar" pasa a ser persistente, porque si no
  el aviso recalculado volveria enseguida; se resetea al cambiar o
  quitar el plato del hueco
- El triangulo del calendario usa tambien el estado real
- Campo en el payload KV (opcional, retrocompatible) y en el esquema
  CloudKit de Development
- 3 tests: marca obsoleta no se lista, regla endurecida despues se
  detecta, y lo ignorado no reaparece

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
2026-09-10 21:54:37 +02:00
alexandrev-tibco 718eef16bf reglas: el panel explica que se incumple y como arreglarlo; stats por etiqueta
- RuleViolation.reasons: el motor devuelve ahora QUE regla rompe cada
  asignacion (repetido en la semana, maximo por semana superado, no
  consecutivo, no mismo dia, solo comida/cena, solo entre semana o fin
  de semana) con su etiqueta y limite
- Panel de avisos: bloque con cada regla rota en lenguaje claro + una
  sugerencia de que hacer, y boton nuevo "Cambiar plato" que abre el
  selector de ese hueco (antes solo se podia quitar o ignorar)
- Estadisticas: matriz "Etiquetas por tipo de comida" (p.ej. cuantas
  cenas son de pescado) y "Platos por etiqueta" del catalogo
- Tests: 4 casos de las razones de incumplimiento; harness que renderiza
  las secciones nuevas de stats a /tmp/stats_render.png
- Strings en 6 idiomas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
2026-09-10 21:06:42 +02:00
alexandrev-tibco 715dd4ec5e export: revision UX de estilos, 2 estilos nuevos y copia como texto
- Header elegante: logo fijado a la izquierda y titulo centrado de verdad
  (ZStack), fecha a la derecha
- School transpuesto: dias como columnas y comidas como filas — antes con
  1-2 comidas quedaba media pagina A4 apaisada en blanco
- Default: las celdas del grid se expanden para llenar la pagina (mismo
  sintoma de hueco que el school)
- Estilo nuevo "Story" (9:16, 2160x3840) para stories de IG/WhatsApp y
  "Minimal (imprimir)" en A4 con lineas finas sin fondos (ahorro tinta)
- Picker del preview pasa de segmentado a chips desplazables (5 estilos)
- WeekPlanTextExporter + boton "Copiar como texto (WhatsApp)" en el
  preview: menu semanal con *negritas* y emojis listo para pegar en
  WhatsApp/Telegram (evento analytics format=text)
- Harness de render en tests: genera PNG de los 5 estilos con datos de
  ejemplo en /tmp/mealmood-export-previews para revisar el diseno
- Nombres de estilos en espanol corregidos (estaban en ingles)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
2026-09-01 16:04:07 +02:00
alexandrev-tibco 39d3534c57 fix: el tap del microfono seguia aislado al main actor (2.0.3 build 68)
El fix anterior arreglo los permisos —el crash se movio de start a beginSession,
y de TCC a AVAudioNodeTap::CheckEmitBuffer— pero el tap seguia petando.

Mi error: quitar el acceso a self del closure no elimina la inferencia de
aislamiento. La inferencia viene de DONDE se escribe el closure, no de lo que
captura. Escrito dentro de un metodo @MainActor, seguia siendo main-actor
isolated, y el tap se dispara desde el hilo de audio en tiempo real.

Ahora el handler se construye en makeTapHandler, que es nonisolated, y se pasa a
installTap. La estructura se auto-verifica: quitarle nonisolated a makeTapHandler
no compila, porque installTap es nonisolated y no puede llamar a un metodo
aislado. El bug pasa de crash en produccion a error de compilacion.

Tests: testTapHandlerRunsOffTheMainThread invoca el handler desde una cola de
fondo, que es exactamente la condicion que trapeaba. No hace falta microfono; el
intento anterior con AVAudioEngine se saltaba siempre porque el simulador no
tiene entrada de audio utilizable.

Barrido del mismo patron en el resto de servicios @MainActor: CalendarService y
NotificationService usan las variantes async/await, que estan anotadas y no
tienen closures. SpeechDictationService era el unico sitio.

Lanes feedback y crashlog para leer los reportes de TestFlight desde la API:
spaceship apunta a v1/betaFeedbacks, que Apple ya retiro; el endpoint vivo es
v1/apps/<id>/betaFeedbackCrashSubmissions y el log viene inline en logText.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
2026-08-20 09:21:52 +02:00
alexandrev-tibco 896c78d260 fix: crash al dictar por closures de callbacks aislados al main actor
Crashlytics: dispatch_assert_queue_fail en com.apple.root.default-qos, dentro de
closure #1 in SpeechDictationService.start, llamado desde __TCCAccessRequest.

El proyecto compila en Swift 6 (activo desde febrero, antes del dictado). Los
callbacks de APIs ObjC pre-concurrency escritos dentro de este tipo @MainActor se
infieren aislados al main actor, y Swift 6 inserta una comprobacion de aislamiento
al entrar. TCC invoca el handler en cola de fondo, la comprobacion falla y mata el
proceso. El Task { @MainActor } de dentro no ayuda: el crash es antes de llegar.

Cuatro sitios con el mismo patron, tres enmascarados detras del primero porque
nadie pasaba de los permisos:

- requestAuthorization y requestRecordPermission -> wrappers nonisolated con
  continuation, el handler deja de estar aislado
- recognitionTask -> closure @Sendable explicito; solo cruzan valores Sendable
  porque SFSpeechRecognitionResult no lo es
- installTap -> captura el request local en vez de ir por self, que el hilo de
  audio en tiempo real no debe tocar estado del main actor

requestAuthorization llama al handler aunque el permiso ya este concedido, asi
que afectaba tambien a usuarios recurrentes: el dictado probablemente no ha
funcionado nunca desde la 2.0.

Los tests que acompanan NO reproducen el crash: el simulador entrega el callback
sincrono en el hilo principal y el codigo previo pasa igual. Comprobado
revirtiendo el fix. Queda pendiente validarlo en dispositivo real.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
2026-08-19 21:28:29 +02:00
alexandrev-tibco 3694297d4f 2.0: mostrar los dos ultimos pasos del onboarding (aha moment + paywall)
completeOnboarding escribia settings.onboardingCompleted al terminar el paso
de platos. Ese flag es el que ContentView observa para cambiar de OnboardingView
a HomeView, asi que el save destruia el flujo antes de que nextStep() pudiera
pintar el paso 5. WeekReadyStepView (el aha moment de la 1.2.0) y PaywallStepView
no se han visto nunca. GA4 lo confirma: 3 onboarding_completed en 28 dias y cero
eventos con step 5 o 6.

Se separa persistir los datos de marcar el onboarding como terminado:
completeOnboarding(markFinished:) commitea platos y plan sin tocar el flag, y el
nuevo finishOnboarding() lo escribe al salir del paywall. HomeView ya difiere su
prompt premium a la segunda sesion, asi que no hay paywall duplicado.

De paso, los nombres de paso en AnalyticsService estaban desalineados: el indice
5 decia "5_paywall" cuando el paso 5 es WeekReady, y el 6 no tenia nombre.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
2026-08-05 20:53:10 +02:00
alexandrev-tibco 4ffa15b06a 2.0: CloudKit private-database sync replaces KV snapshot sync
Foundation for 2.1 family sharing (CKShare needs these models + container).
SwiftData cannot share across Apple IDs today, so 2.0 ships true multi-device
sync for the same account instead:

- Models made CloudKit-compatible: dropped @Attribute(.unique) on all 5,
  inline defaults on every attribute, WeekPlan.slots stored as optional
  relationship (name preserved → lightweight migration) with non-optional
  slotList facade; ~73 call sites renamed.
- Container: cloudKitDatabase .automatic, falling back to the local-only
  store when CloudKit is unavailable; failures recorded to Crashlytics.
- Entitlements: iCloud CloudKit service (container already existed);
  remote-notification background mode for push-driven sync.
- Legacy KV snapshot sync (ICloudSyncService) stays inert when CloudKit is
  active — kept only for 1.x devices.
- DeduplicationService collapses cross-device duplicates deterministically on
  launch (settings singleton, default tags with tagId remapping, same-week
  plans).
- Note: AppSettings.isPremium now syncs across same-Apple-ID devices; StoreKit
  (PremiumSyncService + Transaction.updates) remains the source of truth and
  reconciles on every launch/foreground.

All 21 unit tests pass, including ICloudSyncPremiumIsolationTests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
2026-07-12 18:13:45 +02:00
alexandrev-tibco 110807d239 1.0.5: Eating out slots, rule violations panel, Crashlytics, search bar fix
- Eating out: tap any empty slot → "Mark as eating out"; shows teal indicator,
  skipped by auto-assign, counts as complete, tap again to unmark
- Rule violations panel: access via wand long-press context menu when conflicts exist;
  shows all isRuleOverridden slots with Fix (clear) or Ignore (acknowledge) actions
- Firebase Crashlytics integrated: CrashlyticsService + dSYM upload build phase,
  isPremium property tracked per session
- PremiumSyncService: extracted premium state machine, StoreKit Transaction.updates
  listener, isPremium no longer synced via iCloud to avoid stale state
- StoreManager: analytics on purchase/restore, bundle ID fallback for product ID lookup
- Search bar contrast bug fixed: TextField now has explicit foreground color for dark mode
- WelcomeStepView: redesigned onboarding welcome screen with week preview
- Version bump: 1.0.5 build 22

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 09:48:20 +02:00
alexandrev-tibco a532afbee6 Stabilize planner UI, fix reset flow, and force Premium on TestFlight 2026-02-27 13:09:58 +01:00
alexandrev-tibco 9f4cf7484d Fix dish deletion list flicker with stable identity/order and add regression test 2026-02-26 11:06:54 +01:00
alexandrev-tibco e593453abd Version casi lista 2026-02-17 13:34:02 +01:00