Auto-relleno (#5): SnapshotRepository.autoFillGap crea snapshots interpolados
linealmente entre los dos snapshots que bordean el hueco, marcados isEstimated.
deleteEstimatedSnapshots para borrado en bloque. Botón 'Autocompletar N meses'
(wand.and.stars) en DataGapsSheet + 'Añadir manualmente'. Idempotente. Strings 7 idiomas.
Widgets/Lock Screen (#8, vía agente): familias enriquecidas — systemSmall/Medium/Large
con net worth + cambio desde check-in + racha + próximo check-in + progreso de objetivo;
lock screen accessoryCircular (gauge objetivo/milestone), accessoryRectangular, accessoryInline.
Deep link quickupdate preservado. Solo InvestmentWidget.swift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Dedup prevención: UUIDs estables (StableID v5) asignados al default Account y a las
categorías por defecto en createDefaultAccountIfNeeded, ensureDefaultCategoriesExist,
createDefaultCategories y el reset de SettingsViewModel → CloudKit fusiona nativamente,
no más acumulación de duplicados.
isEstimated: nuevo atributo Bool opcional (default NO, syncable) en Snapshot para marcar
snapshots interpolados del auto-relleno. Requiere deploy de schema CloudKit antes del
release. Compila verde (verificado antes de que el agente de widgets empezara a editar).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Funde 1.5.1 en 1.6.0. Parte 1 dedup (prevención): StableID (UUIDv5 namespace+nombre),
Account.defaultAccountStableID, Category.stableID(for:) + asignación en
createDefaultCategories. Pendiente: asignar en repos + limpieza de duplicados existentes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
frame_panorama_codex.py / _ipad.py: panorámica premium oscura (Impact, marco real,
+22.7% verde, línea continua) con copy localizado nativo en 7 idiomas. Usado para
las screenshots publicadas de la 1.5.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Feature 1 — Aviso de huecos: SnapshotGapDetector detecta huecos internos por
source (meses sin datos entre dos snapshots). Banner desechable en Dashboard +
DataGapsSheet para rellenar (abre AddSnapshotView con el mes que falta). Complementa
la interpolación de gráficas: la línea se ve suave pero el usuario sabe que faltan
datos reales.
Feature 2 — Resumen mensual compartible: MonthlySummaryShareView (tarjeta con marca:
valor, variación desde el check-in, racha, mood/rating) renderizada con ImageRenderer
y compartida vía ShareService. Botón en la fila "check-in done" del Dashboard.
Feature 3 — Import con vista previa: ImportPreview + previewImportAsync (dry-run
READ-ONLY, no muta nada) cuenta qué se creará/actualizará usando los mismos checks
de existencia que applyImport. ImportDataView muestra la confirmación antes de
importar. El path real de import (applyImport/importDataAsync) NO se toca.
Localización en los 7 idiomas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
El JSON v2 perdía Goals, source.monthlyContribution, category.allocationTarget y
todo el Journal (nota/mood/rating/completionTime). Nuevo formato v3:
- source: + monthlyContribution, customFrequencyMonths
- category: + allocationTarget
- top-level: + goals[] (name/targetAmount/targetDate/isActive/account)
- top-level: + journalEntries[] (monthKey/note/mood/rating/completionTime/createdAt)
Import parsea y aplica todo, creando Goals y JournalEntry DIRECTAMENTE en el
contexto de background (GoalRepository es @MainActor → no usable desde el import).
Dedup: goals por nombre+cuenta, journal por monthKey → reimport idempotente.
Retrocompatible con JSON v2. CSV sin cambios.
Guard: si el import trae journal, se salta el marcado de completions derivado de
snapshots (evita JournalEntry duplicados por el desfase de merge viewContext).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Nueva utilidad ChartGapFill: para cada source, rellena los meses sin snapshot
entre dos conocidos con interpolación lineal (Feb=100k, Jun=130k → Mar/Abr/May
~110/120/130k), arrastra el último valor tras el último snapshot y no aporta
antes del primero. Solo afecta al pintado de gráficas — NO crea ni modifica
snapshots (cero riesgo para la sync iCloud). Aplicado a las evolution del
Dashboard (total + por categoría) y de Charts. Toggle en Settings "Smooth Gaps
in Charts" (ON por defecto), localizado en los 7 idiomas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
El import corre en un contexto de background, pero MonthlyCheckInStore muta
viewContext (main). Llamar setCompletionDate desde el background era una
violación de concurrencia de Core Data → los JournalEntry.completionTime de los
meses importados no persistían, dejando el "siguiente check-in" anclado a una
entrada vieja (p.ej. May 2025). Envuelto el marcado en viewContext.performAndWait
para ejecutarlo en la cola correcta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
createSnapshot hacía save() por cada snapshot, y save() dispara refreshWidgetData
→ checkpointWAL (performAndWait en el viewContext main) + reloadAllTimelines. Con
213 snapshots eran 213 saves + 213 checkpoints en el main → Application Not
Responding. Añadido modo batch a createSnapshot (no guarda por fila; applyImport
ya guarda el contexto una vez al final) y flag suppressWidgetRefresh en
CoreDataStack para neutralizar el refresh por fila durante el import, con un único
refreshWidgetData al terminar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
ImportDataView usa @EnvironmentObject (iapService/accountStore/tabSelection).
En Mac (Designed for iPad) las sheets no heredan el environment vía
PresentationHostingController → EnvironmentObject.error() / SIGTRAP al abrir
Import Data (ImportDataView.swift:247, shouldShowAccountSelection). Reinyectados
los 3 objects en las sheets de Settings, Dashboard y Onboarding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
CRÍTICO: el auto-dedup de build 68 borraba entidades padre con regla Cascade
(Account/Category). Al propagarse el delete por CloudKit, otros dispositivos
aplicaban la cascada al importar ANTES de la reasignación de hijos → wipe de
sources/snapshots. Vació iPhone y iPad. Quitadas las llamadas automáticas a
cleanupLogicalDuplicates() en processRemoteChanges y en la carga; el UUID-dedup
seguro se mantiene. Métodos quedan definidos para un rediseño CloudKit-safe.
Gráficas: forward-fill por source en evolution/allocation/period para que el
total de cada mes refleje el último valor de CADA source (no solo las
actualizadas ese mes) y el último punto coincida con el total del portfolio.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
cleanupLogicalDuplicates() colapsa registros con misma clave de negocio pero
distinto UUID (defaults sembrados por-dispositivo antes del primer import):
Account/Category por name, InvestmentSource por account+name, Snapshot por
source+día+valor, Goal por name+target, JournalEntry por monthKey. Ganador
determinista (createdAt asc, tiebreak UUID) → todos los dispositivos eligen el
mismo superviviente y los deletes convergen sin race. Reasigna hijos al ganador
antes de borrar. Snapshot dedup produce la UNIÓN → sin pérdida de historial.
Cableado en processRemoteChanges (tras import) + carga inicial (limpia lo ya
existente). El dedup previo por UUID no veía estos casos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Materializa el modelo completo en el esquema de Development de CloudKit para poder desplegar
a Production los campos que nunca se exportaron (monthlyContribution, allocationTarget). Sin ellos,
Production rechaza los registros y el export falla en bloque (CKErrorPartialFailure).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
deepErrorReport() sigue NSUnderlyingError y CKPartialErrorsByItemIDKey y extrae el recordType
del CKRecord embebido → dice qué entidad rechaza CloudKit. integrityReport() lista counts por
entidad + registros con id/createdAt nil u orphan snapshots. Ambos en el diagnóstico copiable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
El modelo declara createdAt optional=YES pero las clases lo tenían @NSManaged var createdAt: Date
(no-opcional). Al materializar un registro importado por CloudKit con createdAt transitoriamente
nil, el acceso revienta el merge → contribuye al CKErrorPartialFailure y a que el Goal no sincronice.
Mismo patrón safeValue/setManagedValue ya aplicado a Snapshot (hotfix b76b8f8) que no cubrió estas 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
- EvolutionChartCard: swipe entre 7 charts (evolution, allocation libres; performance,
drawdown, volatility, prediction, year-over-year premium). No premium ve las premium
con candado + CTA de unlock; premium las ve reales. Reutiliza ChartsViewModel (computa
solo la página visible) y las vistas del feature Charts.
- MonthlyCheckInCard: aviso de próximo check-in siempre visible (fecha + badge de
días restantes/atrasado), con fallback cuando no hay check-in previo.
- Localización de nuevas cadenas en los 7 idiomas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
Consulta el export de Crashlytics en BigQuery (dataset firebase_crashlytics,
proyecto portfoliojournal-ef2d7) con el service account de pass. Top crashes por
versión/build. Setup documentado en el docstring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El swipe entre páginas del Home (build 62) y entre charts (build 60) no
funcionaba encima de la gráfica: su DragGesture de scrub (minimumDistance 0)
consumía el gesto. Cambiado a .simultaneousGesture — ambos se reconocen; la
dominancia horizontal en onEnded evita romper el scroll vertical. Ahora el swipe
funciona en toda la card, también sobre el chart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El feedback #4 original apuntaba a la card 'Portfolio Evolution' del HOME, no
solo a la pestaña Charts (donde ya se hizo en build 60). Ahora la card del Home
se desliza entre páginas:
- Evolution (con su toggle Total/By Category intacto)
- Allocation (semicírculo + leyenda con %), datos de viewModel.categoryMetrics
Puntos indicadores, transición direccional, título dinámico. 'By Category' no
es página aparte porque ya es un toggle dentro de Evolution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
- Quick Update: checkClipboard ahora detecta también una IMAGEN copiada
(pb.hasImages) y hace OCR automático — te ofrece pegar el importe detectado
sin pulsar 'Escanear'. Gated por changeCount. El 'Pegar €X' del teclado
también usa el importe OCR'd de la imagen.
- Settings iCloud: botón 'Copiar diagnóstico de iCloud' SIEMPRE visible (el
desplegable de detalle solo salía con un error activo, difícil de encontrar).
Copia versión/build, contadores locales, últimas fechas import/export,
criticidad, hint y el error completo con códigos internos. Strings ×7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
#3 Quick Update paste/scan por campo:
- Barra de teclado con 'Pegar €X' (si hay importe en portapapeles) y 'Escanear'
(PhotosPicker → OCR on-device). Actúan sobre el campo enfocado.
- ImageAmountScanner nuevo (app target, Vision) espejo del scanner del Share
Extension: candidatos rankeados por prominencia, filtra % y años.
- 1 candidato → rellena directo; varios → confirmationDialog para elegir.
- Strings ×7.
#4 Swipe entre charts (iPhone):
- Gesto horizontal en el área del chart avanza al chart anterior/siguiente en
el orden del menú de título (respeta showForecast). Indicador de página con
puntos. Transición direccional. selectChart ya no bloquea premium (muestra
teaser), así que deslizar a uno premium funciona.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
De 7 comentarios de TestFlight:
- Labels de línea ilegibles (puntos 5+7): ChartLabels.showsLabel thin-out
(máx 5 iPhone / 8 iPad, siempre primero y último). Charts multi-serie
(Compare, Period, Year vs Year) etiquetan solo el ENDPOINT de cada serie.
Prediction etiqueta solo el forecast final. Aplicado a Evolution, Rolling,
Drawdown, Volatility, Compare, Period, YoY, Prediction.
- 'Needs update' en meses pasados (punto 2): MonthlyCheckInView.snapshotForViewedMonth
— el estado es por el MES QUE SE VE (effective month de referenceDate), no
contra la última completion global. Diff usa el snapshot de ese mes.
- Selector de periodo Performance (punto 6): slider 1..60 → picker segmentado
consistente con el resto, opciones capadas a los meses de datos disponibles
(availableHistoryMonths). Ya no deja pedir ventanas sin datos.
- Home iPad/Mac (feedback previo): masonry con nº de columnas según ancho
(2 iPad / 3 Mac ancho >=1250), reparto por peso, propio ScrollView.
Pendiente de este lote: Goals no sincronizan (verificado modelo Goal CloudKit-OK
→ es el subset del partial-failure, no filtro ni esquema; necesita códigos
internos del error), paste/OCR por campo (punto 3) y swipe entre charts (punto 4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
Allocation:
- Semicírculo responsive que llena su mitad de la fila (grande en iPad, no
minúsculo en una esquina) + porcentaje dentro de cada segmento >=7%.
SemicircleArc acepta center explícito; labels en el ángulo medio del arco.
Home iPad:
- Ya no columna única estrecha a 720. Hero + check-in a ancho completo, resto
de cards en DOS columnas masonry independientes (sin acoplar alturas → sin
gaps del grid). Cap 1100pt.
Year vs Year KPIs:
- Retorno del año actual + diferencia (pp) vs año anterior + mejor/peor año
histórico. Usa forecastEndValue para el año en curso.
Fixes:
- 'View all achievements' en Journal no hacía nada: NavigationLink inline
(frágil según contexto de navegación) → sheet fiable en cualquier sitio.
- iCloud: CKErrorPartialFailure tras una sync que funciona ya NO se pinta en
rojo alarmante — nota calmada 'Sincronizando con iCloud' + detalle técnico
en disclosure. Rojo solo si nunca ha sincronizado y es fallo total.
syncErrorIsCritical + strings ×7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
Feedback del usuario en iPad:
- Allocation: gráfica de tarta → semicírculo estilo hemiciclo (parlamento).
SemicircleAllocation con arcos dibujados a mano (SemicircleArc Shape),
segmentos por cuota en 180°, total/selección en el centro, tap por segmento.
- Charts de Analyze (Compare, Period vs Period, Year vs Year) ahora SÍ muestran
KPIs en la cabecera de iPad como el resto: Compare (líder/rezagado por modo),
Period (retorno final por periodo), YoY (retorno final por año seleccionado).
- Momentum & Streaks en el sidebar del Journal (iPad, 320pt): variante compact
— tiles en fila sin subtítulos que rompían a 3 líneas, sin bloque de logros.
iCloud:
- CKErrorPartialFailure (CKErrorDomain code 2) mostraba un mensaje opaco.
CoreDataStack.hint(for:) recorre CKPartialErrorsByItemIDKey y clasifica el
código dominante → hint accionable (esquema/quota/red). Settings muestra el
hint legible sobre el detalle técnico. lastSyncErrorHint publicado. Strings ×7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
Dos crashes de producción con la misma causa raíz: atributos declarados
no-opcionales en Swift (@NSManaged id: UUID, date: Date) que llegan nil en
runtime — NSPersistentCloudKitContainer materializa registros de otros
dispositivos por fases y los objetos borrados pueden seguir referenciados por
vistas vivas. El force-bridge de nil revienta:
1. UUID._unconditionallyBridgeFromObjectiveC en ForEach (keypath Identifiable.id)
2. Date._unconditionallyBridgeFromObjectiveC en ChartsViewModel.monthlyTotals
Fix sistémico (no por call-site): NSManagedObject.safeValue(forKey:fallback:healing:)
lee el primitive y cae a un valor seguro; id se auto-cura (UUID nuevo escrito al
primitive sin ensuciar el objeto). Aplicado a:
- Snapshot: id (heal), date, createdAt (fallback distantPast)
- InvestmentSource / Goal / Account: id (heal), name (fallback "")
- Category: id (heal), name, colorHex, icon (fallbacks neutros)
Los accessors son @objc → NSSortDescriptor(keyPath:) y KVC siguen funcionando.
awakeFromInsert/awakeFromFetch se mantienen. Smoke test UITest en verde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
TipKit (Tips.configure con frecuencia diaria):
- QuickUpdateTip: popover en el '+' del Home — descubre Quick Update
- ScreenshotOCRTip: inline en Quick Update — descubre el OCR por captura
- SiriTip: en el cierre del check-in guiado — descubre el intent de voz
- ChartShareTip: popover en el compartir de Charts
- Se invalidan al ejecutar la acción; máx 1-2 impresiones cada uno
- Strings ×7 idiomas
Onboarding:
- Al completar el onboarding sin datos, se abre Add Source directamente
(notificación .openAddFirstSource) — el usuario nuevo no aterriza en un
dashboard vacío
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El modal estaba congelado en las features de 1.4.0 (hardcodeadas en la vista) y
el título decía 'What's New in 1.4' fijo. Rediseñado como catálogo por versión:
- WhatsNewCatalog: una entrada por release (1.4.0/1.4.1/1.4.2/1.5.0) con sus
features; AÑADIR ENTRADA CON CADA RELEASE (comentario en el código).
- Diff ACUMULATIVO desde lastSeenWhatsNewVersion: saltar 1.3.x → 1.4.2 muestra
1.4.0 + 1.4.1 + 1.4.2 agrupadas con badge de versión (headers solo si hay
más de una release pendiente). Comparación numérica de versiones (1.10 > 1.9).
- lastSeen vacío + onboarding ya completado (usuarios pre-1.4.0) = mostrar todo
el catálogo; instalación nueva = lastSeen sella a la versión actual al
completar onboarding (un usuario recién onboardeado no ve 'novedades').
- Sin entrada en el catálogo para el salto → no se muestra sheet.
- Título dinámico (whats_new_title_format) y contenido scrollable.
- Strings de 4 releases + título ×7 idiomas.
Verificado en simulador con -lastSeenWhatsNewVersion 1.4.0: pending
[1.5.0, 1.4.2, 1.4.1] y sheet agrupada correcta.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
Home iPad/Mac (feedback del usuario: 'me gusta más el iPhone'):
- Fuera el grid de 2 columnas (acoplaba alturas de fila y dejaba huecos entre
cards de tamaños distintos) → columna única centrada a 720pt, patrón
Things/Day One. Eliminado el drag-drop del grid, columnSpan y el overlay
full-screen (~120 líneas); el orden/visibilidad sigue en Customize.
Goals:
- ProgressRing nuevo (anillo estilo Fitness con porcentaje centrado y color
semántico: verde=logrado, ámbar=retrasado, acento=en camino) en GoalRowView
y en la card de Goals del Home. GoalProgressBar queda solo en la share card.
Journal:
- Momentum & Streaks vive ahora en la cabecera del Journal (la historia del
hábito es narrativa, no dashboard).
- Filas timeline: círculo de mood como ancla visual (tint semántico por mood),
mes + estrellas + preview de nota.
Sources:
- Badge 'Needs update' como chip ámbar visible; valor en tipografía rounded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El ritual mensual como flujo paso a paso en vez de formulario:
- Una fuente por pantalla: valor anterior a la vista, entrada grande (40pt
rounded), delta en vivo (chip verde/rojo con importe y %), sugerencia de
portapapeles, 'Igual que el mes pasado' y 'Omitir'
- Barra de progreso del ritual; cancelable solo al inicio (interactiveDismiss
bloqueado a mitad para no perder valores)
- Reflexión: mood (5 estados existentes) + nota de una línea
- Cierre: total nuevo, delta del mes, streak, compartir y haptic de éxito
- Persistencia por los MISMOS caminos que Quick Update (Snapshots) y el journal
(MonthlyCheckInStore → JournalEntry): coherente con el resto de entradas
- Entrada: CTA del check-in card en Home (fullScreenCover); la fila de
completado sigue navegando al journal
- Fix: referenceDate congelada en @State al presentar — el card la recalcula al
guardar y SwiftUI re-evaluaba el contenido del cover cambiando el mes a mitad
de flujo
- --pending-checkin en ScreenshotMode (revierte el seed a 'mes pendiente') +
UITest testGuidedCheckInFlow end-to-end (valor → skips → mood → cierre) ✓
- Strings ×7 idiomas
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
Sistema:
- AppBackground: fuera gradiente + formas decorativas → systemGroupedBackground
plano en toda la app. La profundidad la da la jerarquía, no la decoración.
Home (3 zonas: estado / acción / contexto):
- TotalValueCard: hero tipográfico — número 44pt rounded sobre card limpia,
chip de delta semántico (verde/rojo), sparkline de evolución 56pt estilo
Stocks, fila secundaria YoY / inception / forecast. Fuera el gradiente azul.
- MonthlyCheckInCard state-driven: pendiente → protagonista con progreso real
de fuentes (N de M) y un CTA; completado → fila discreta con checkmark,
próximo check-in y streak integrado. En iPad va a ancho completo bajo el
hero (zona de acción), fuera del grid.
- InsightsRow: de carrusel de chips a un insight único rotatorio (tap para
ciclar, dots de posición).
- Eliminados del top: streak badge (vive en el check-in) y banner de pending
updates (el progreso del check-in lo comunica).
- Defaults nuevos: momentumStreaks/pendingUpdates/periodReturns/monthlySummary
ocultos por defecto (siguen disponibles en Customize; configs guardadas de
usuarios existentes se respetan).
- Strings nuevas en 7 idiomas (checkin_done_*, checkin_sources_progress).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El círculo decorativo de AppBackground (70% del ancho del panel, offset -35%)
se desborda de su panel por diseño y en layouts side-by-side queda flotando
SOBRE el sidebar de Charts. Las shapes de SwiftUI participan en hit-testing por
defecto y el panel derecho va después en el HStack → cada tap en la zona del
sidebar cubierta por el círculo moría en silencio. El patrón lo delató: fallaban
Overview + Analyze (arriba, bajo el círculo) y funcionaban Risk + Forecast
(abajo). Dependía de la geometría (orientación/tamaño), por eso no reproducía
en el simulador en portrait.
- AppBackground: .allowsHitTesting(false) — un fondo decorativo jamás debe
interceptar toques (fix global: aplica también a Sources/Journal en iPad)
- Panel de detalle de Charts: .clipped() para que la decoración tampoco PINTE
sobre el sidebar
- Selección premium nunca se bloquea: los charts premium se seleccionan y
muestran teaser de desbloqueo en el área del chart (chart_locked_* ×7 idiomas);
el paywall se presenta desde el botón (contexto fiable)
- UITests: testChartSidebarSelection ahora corre en landscape (geometría que
reproducía el bug) + testChartSelectionWithoutPremium nuevo; --no-premium
en ScreenshotMode para testear la experiencia free
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El bug de Allocation/Contributions que no respondían en iPad persistía tras dos
fixes de lógica — la causa estaba en la capa de interacción (Buttons custom en
LazyVGrid dentro de ScrollView dentro de NavigationSplitView). Rediseño completo
alineado con el HIG:
- iPad: List(selection:) nativa con estilo .sidebar y secciones agrupadas
(Overview/Analyze/Risk/Forecast) — hit-testing del sistema, navegación por
teclado, pointer effects y VoiceOver gratis. El gating premium sigue en
selectChart vía el binding de selección.
- Filtros (Group/Category/Sources) → Menu nativo en la toolbar con checkmarks
e icono con badge cuando hay filtros activos. Desaparecen las pills apiladas.
- iPhone: toolbarTitleMenu — el título es el selector de chart (patrón
Files/Freeform); fuera el carrusel de 14 chips. Periodo como segmented picker
estilo Stocks encima de la gráfica.
- Slider de Performance integrado en el toolbar del chart (iPad) y como card
compacta (iPhone).
- Eliminadas ~300 líneas de selectores custom (tiles, pills, chips).
Verificado con UITest nuevo (testChartSidebarSelection) en simulador iPad Pro:
tap en Allocation/Contributions/Rolling 12M carga cada chart, y el filtro de
periodo de Rolling reacciona. ScreenshotMode ahora activa debugPremiumOverride
para poder testear charts premium.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
Bugs:
- Charts bloqueadas (intermitente) y tiles que no responden: el observer Combine
registraba el estado como actualizado ANTES de llamar a updateChartData, cuyo
guard de reentrada descartaba la llamada en silencio — reintentarlo era no-op
permanente. Ahora el bookkeeping vive dentro de updateChartData, las llamadas
reentrantes se colean en vez de descartarse, y selectChart computa en síncrono
(pulsar un tile es determinista y re-pulsar actúa de retry).
- Paywall sheet: movida del Group que cambia con el size class a cada layout —
en NavigationSplitView no llegaba a presentarse (tiles premium 'no hacían nada').
- Rolling 12M: el filtro de periodo no filtraba — el cálculo necesita histórico
completo para el lookback, pero ahora la salida respeta selectedTimeRange.
KPIs:
- El header de iPad muestra KPIs específicos del chart activo (los mismos que su
stats row) en vez de las 5 métricas de cartera fijas; las stats rows dentro de
las cards se ocultan en regular width para no duplicar (YoY opta por quedarse
al no tener equivalente en el header).
Compartir:
- Botón de compartir por chart (toolbar iPad + navbar iPhone): renderiza la
gráfica en una card con branding (BrandMark, KPIs, tagline, QR al App Store
con ct=chart_share) vía ImageRenderer y abre el share sheet con imagen + link.
- drawingGroup() se desactiva durante el export (ImageRenderer no rasteriza
capas Metal — salían en blanco). Strings nuevas en 7 idiomas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
LogSnapshotIntent: 'Registra el saldo de Indexa' por voz o desde Atajos, sin
abrir la app (openAppWhenRun=false). SourceEntity expone las fuentes a
Siri/Atajos desde el mirror del App Group; el valor entra por la misma cola
que el Share Extension y se materializa como Snapshot al instante (el intent
corre en el proceso de la app). Diálogo de confirmación con formato de divisa
de la fuente.
AppShortcuts.xcstrings: las 7 frases de los 3 shortcuts localizadas a
es-ES/de/fr/it/ja/pt-BR — Siri en español ya funciona. Strings de UI de
Atajos (títulos, parámetros, diálogos) añadidas a los 7 Localizable.strings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El extension acepta ahora imágenes además de texto: al compartir un screenshot
del banco/broker, Vision reconoce el texto EN EL DISPOSITIVO (la imagen nunca
sale del teléfono), extrae los importes candidatos y rellena el más prominente
visualmente (en las UIs bancarias el balance es el número más grande). El resto
de candidatos se ofrecen como chips de un toque. Percentajes y años se filtran.
- ExtImageAmountScanner.swift: VNRecognizeTextRequest + ranking por altura de
bounding box + dedup por valor
- ShareViewController: carga UIImage/URL/Data del provider, estado de escaneo,
sección de candidatos
- Info.plist: NSExtensionActivationSupportsImageWithMaxCount=1
- Strings nuevas en los 7 idiomas (ext_scanning_image, ext_detected_amounts,
ext_ocr_no_amounts)
- PredictionEngine: troceado ternario que agotaba el type-checker en Debug
- Bump a 1.4.2 build 48
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
El archive fallaba en ValidateEmbeddedBinary con firma mixta (extension Manual
+ app/widget Automatic). Los tres targets usan ahora Apple Distribution con sus
perfiles App Store en Release; Debug sigue en Automatic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe