Dos cosas que faltaban, las dos visibles en domingo con el fin de semana
desactivado:
El envio al reloj solo llevaba la semana en curso, asi que no habia forma de
ver las semanas ya planificadas por delante. Ahora viajan cuatro (la anterior,
la actual y las dos siguientes) y la vista de semana se desliza entre ellas,
arrancando en la actual.
Y un dia fuera del plan dejaba al reloj sin nada que enseñar: el domingo no
existe en el payload si no planificas fines de semana, asi que la complicacion
salia vacia y la app decia "abre MealMood en el iPhone", como si fuera un
problema de sincronizacion. Ahora se distingue: si hoy no se planifica se dice,
y se enseña el proximo dia que si tiene comidas — que es lo util en el reloj.
De paso, las semanas se comparan con el calendario alineado a lunes. Con el del
sistema, en las regiones donde la semana empieza en domingo, un domingo caia en
otra semana que su propio lunes y un envio recien hecho parecia caducado.
Refs #36, #37
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
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
2.1.1 ya estaba READY_FOR_SALE con el build 88, asi que Apple rechazo el 89 por
version repetida (90062). La 2.x continua en 2.1.2, que es lo que llevan ahora
los tres Info.plist con version hardcodeada y MARKETING_VERSION.
Incluye la configuracion de dias y comidas por semana (premium) y el arreglo del
Apple Watch, que enseñaba el dia en que se genero el envio en vez de hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
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
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
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
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
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
El panel de avisos solo se alcanzaba desde el menu "..." de la barra, asi
que los incumplimientos pasaban desapercibidos. Ahora, cuando la semana
rompe reglas, aparece un banner bajo el selector de semana con el numero
de comidas afectadas, un resumen de donde estan (Lun - Cena, Mie -
Comida) y un boton Ver que abre el panel con la explicacion y el arreglo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- 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
En premium la barra de navegacion lleva cuatro botones a la izquierda y
mas a la derecha; el ToolbarItem(.principal) se comprimia y iOS
recortaba las flechas, dejando solo el titulo — por eso no aparecian en
el iPhone del usuario aunque en el simulador (modo free, dos botones) se
vieran bien. Ahora el selector es una barra propia bajo la navegacion:
flechas en circulo blanco con borde coral y el rango de semana con
chevron.down al centro. Verificado con captura forzando los botones
premium.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
Los chevrons finos coral sobre la barra ya coral eran invisibles en la
practica — un usuario premium no encontraba como planificar la semana
siguiente. Ahora son botones circulares blancos con chevron en negrita,
y el titulo lleva un chevron.down que delata que abre el selector de
semana. UI test de regresion: navegar a la semana siguiente debe
renderizar sus huecos sin paywall (la creacion del plan al navegar ya
funcionaba, verificado con captura).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- El primer push se descartaba: updateApplicationContext se llamaba antes
de que WCSession terminara de activarse (activacion asincrona). Ahora
la sesion se activa al arrancar la app (ContentView), el payload
pendiente se envia al completarse la activacion, y ademas el reloj
PIDE los datos al abrirse (sendMessage despierta al iPhone en segundo
plano y responde con el snapshot del app group)
- Rediseno: Hoy con cabecera del dia en coral y tarjetas degradadas por
comida (estilo Weather); Semana con circulo de dia a la izquierda y
hoy relleno en coral (estilo Calendar); complicacion con iconos
tintados por comida y widgetAccentable
- Verificado con capturas en simulador de Watch sembrando el app group
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- Target MealMoodWatch (watchOS 10, SwiftUI): vista de hoy y de la
semana en TabView vertical; recibe el snapshot del iPhone por
WatchConnectivity (updateApplicationContext) y lo guarda en su app
group para la complicacion
- Target MealMoodWatchWidget: complicacion accessoryRectangular/
Circular/Inline con las comidas de hoy (heuristica comida/cena por
hora); icono del watch generado desde el logo
- WatchWeekPayload compartido entre iOS app, widget iOS, watch app y
complicacion — llega ya localizado desde el iPhone
- Widget iOS nuevo "Semana" (systemMedium/Large) con los 7 dias;
el widget de hoy pasa a WidgetBundle
- Targets creados via gema xcodeproj (sin abrir Xcode); embed del watch
antes del script de Crashlytics para evitar ciclo de build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
Guideline 3.1.2(c): Apple rechazo la 2.0.5 (77) por no tener enlaces
funcionales a la politica de privacidad y los terminos de uso dentro
del paywall. Anadidos en PremiumView y en el paywall del onboarding
(mealmood.app/privacy.html y terms.html), y ambos enlaces al final de
la descripcion del App Store en los 6 idiomas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- MealSlot.isSkipped: opcion "No planificar esta comida" en el picker de
hueco vacio — cuenta como resuelto (semana completa, notificaciones,
stats) y el autocompletado lo respeta; vista gris con guion, tap para
desmarcar; viaja en undo, snapshot KV (junto con isEatingOut, que
faltaba en el payload) y exports (— en imagen, omitido en texto)
- ShoppingItem.isDismissed: "Ya lo tengo" por ingrediente (swipe
izquierdo) y "Ya esta hecho" por plato entero (boton en la cabecera de
su seccion); van a la seccion "Ya en casa" con restauracion de un tap,
y quedan fuera del export de texto. No se borran porque reconcile los
recrearia
- Esquema CloudKit Development: CD_isSkipped, CD_isDismissed y el record
type CD_ShoppingItem entero, que no existia — la lista de la compra no
estaba sincronizando entre dispositivos (mismo bug que photoData)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- 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
En school/vertical el logo centrado sobre el titulo quedaba mal; ahora
va a la izquierda con el rango de semana y el titulo alineados al lado,
y la fecha de export a la derecha.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- Logo real de la app en el header de los 3 estilos (antes solo en el
default, y pequeno) y en el footer junto a un wordmark MealMood en
degradado coral->verde con tagline localizada en cursiva
- "Powered by MealMood" y "Scan me" estaban hardcodeados en ingles;
ahora share_footer_tagline y share_footer_scan en 6 idiomas
- Titulo del header default con el mismo degradado; QR con borde coral
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- WeekExportPreviewSheet: preview renderizado del export con picker
segmentado de los 3 estilos (cachea cada render); compartir usa el
estilo elegido sin tocar el predeterminado (salvo primera vez)
- Sustituye al WeekExportStylePickerSheet de thumbnails y al doble
sheet picker+share de HomeView; strings nuevas en 6 idiomas
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- MealSlot.secondaryDishId (opcional, aditivo CloudKit); se limpia al
sustituir/quitar/vaciar/marcar comer fuera y viaja en swaps, copia de
semana anterior, undo y snapshot KV de iCloud (retrocompatible)
- Picker de hueco lleno: toggle "anadir como segundo plato" + boton para
quitarlo; HomeViewModel.assignSecondaryDish/removeSecondaryDish
- Se muestra "Plato + Segundo" en calendario, widget y export; la lista
de la compra incluye los ingredientes del segundo; stats lo cuentan
- Evento analytics secondary_dish_added; strings en 6 idiomas
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- Dish.fixedDayOfWeek/fixedMealType (opcionales, aditivos para CloudKit)
- Toggle + pickers de dia/comida en el formulario de plato
- AutocompleteEngine: pre-pasada que asigna los platos fijos a su hueco
antes del bucle MRV, de modo que cuentan para el resto de reglas
- Evento analytics dish_fixed_slot_set; strings en 6 idiomas
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- attemptAddDish/attemptCreateDish: el limite gratuito se comprueba en el
tap de anadir plato (DishListView y los 3 puntos de Home) y muestra el
DishLimitModal -> paywall directamente; antes solo se comprobaba al
guardar el formulario y el usuario podia rellenarlo entero para nada
- DishLimitModal compartido (era private de DishFormView) y
PaywallPresentation promovido a PremiumView.swift; DishListView usa
sheet(item:) tambien para el banner (source dish_counter vs dish_limit)
- Boton en la toolbar de Mis Platos que alterna orden alfabetico
(persistido en dish_list_alphabetical_sort); borrado por swipe corregido
para usar la lista mostrada
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- requestForExistingPremiumUser(): prompt nativo una sola vez para quien
ya era premium antes del ask post-compra (evento review_prompt_premium_user)
- Funnel de semanas completadas emite review_funnel_shown/positive/negative/dismissed
- Paywall de Home via sheet(item:) con PaywallPresentation: el source viaja
con la presentacion; antes dos @State separados hacian que a veces se
registrara paywall_viewed con source=unknown (post_onboarding nunca llegaba)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
- Tras una compra exitosa se pide la review nativa (una vez por usuario,
con 2s de delay para que se vea el estado premium activo)
- El boton de ajustes abre el formulario de resena del App Store via
?action=write-review en vez de SKStoreReviewController, que Apple
limita a 3 prompts/ano y el resto de veces no muestra nada
- Evento analytics review_prompt_post_purchase para medirlo
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
La 2.0.3 nunca llego a la App Store, solo a TestFlight, asi que el numero queda
libre y evita saltarselo. El binario lleva la version dentro, y ASC solo adjunta
a una version las builds cuyo CFBundleShortVersionString coincide, asi que la
build 69 no se puede reetiquetar: hay que reconstruir.
Codigo identico al de la build 69, ya verificada en dispositivo.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt