Rediseño de las cards de compartir con el sistema visual de las screenshots
ASO: fondo navy con glows y grid, sparkline de evolución con gradiente
azul→cian y punto final, chip de delta, footer con marca + QR. Aplica a
Portfolio Snapshot, Monthly Check-in y Monthly Summary.
Potencia referral: sheet de opciones previo con preview en vivo
(PortfolioShareOptionsView) — toggle «Solo porcentajes» (sin importes
absolutos, la fricción #1 para compartir) y formato Post/Story 9:16. QR y
link con ?ct=snapshot_share para atribución en App Analytics. Analytics:
share type portfolio_value_percent. L10n ×7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
showsTitle (default true) en HorizontalPerformanceChart, DrawdownChart,
VolatilityChartView, PredictionChartView y YearOverYearChartView; el
pageContent del Dashboard pasa false porque el header de la card ya nombra
la página. La sección Charts no cambia.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
Feedback TF build 79: fuera el swipe entre gráficas (navegación = chip bar;
el gesto chocaba con scrub/pan/brush). Brush con gesto unificado decidido en
touch-down: cerca de un asa (18pt) redimensiona; en cualquier otro punto
(dentro de la ventana o sobre el historial atenuado) desliza la ventana entera
manteniendo el ancho — seleccionar ene–mar y arrastrar hasta jun–ago. Con
ventanas estrechas las asas solo agarran desde fuera, el interior siempre mueve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
Feedback TestFlight build 78: arrastrar las asas del minimapa disparaba el
swipe entre charts (el brush vivía dentro de chartContent, bajo el
simultaneousGesture del swipe). El brush sale del chart y pasa a la barra de
periodo, fuera del área con gesto.
Y de paso, el cambio de diseño pedido: la ventana elegida ya no es zoom visual
por-chart sino ChartsViewModel.customRange — filtra los DATOS de todas las
gráficas (evolution, contributions, rolling, drawdown, volatility, cashflow,
comparison, YoY, prediction). Sparkline del brush desde fullHistorySeries
(agregación mensual extraída de calculateEvolutionData como monthlySeries).
Presets limpian el rango custom; selección completa = sin filtro. El pinch
por-chart se mantiene como zoom visual.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
Fix look&feel (#29): chip bar selector visible (sustituye page dots + title menu
como navegación primaria), sin bubbles permanentes en compact (solo endpoint),
Performance a barras horizontales con >6 categorías, línea con gradiente y
estética de widget, pinch anclado al centro + doble-tap reset + haptics.
Workable (#30): modelo de ventana (inicio,fin) único para pinch/pan/brush;
minimapa bajo demanda con asas snap-a-mes y ventana arrastrable; label de rango
con pickers mes/año precisos; header vivo (valor+delta del rango, scrub con
haptic); drill-down por tap en Allocation/Performance → Evolution filtrada
(premium, chip Filtrado ✕ para volver); overlay aportaciones acumuladas
(premium); insight contextual del rango visible (premium). L10n ×7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
#10 Ocultar saldos: BalancePrivacyManager (balancesHidden + requireBiometricToReveal),
modifier .hiddenBalance() (blur + máscara de bullets, ancho estable), botón ojo en Dashboard,
revelar con Face ID vía AppLockService, sección Privacidad en Settings. Aplicado a total
portfolio, forecast, sources, goals, monthly summary.
#11 Siri/App Intents: NetWorthIntent ('¿cuál es mi patrimonio?' → 'Tu patrimonio es X'),
StreakIntent, 5 AppShortcuts con frases. Widget interactivo iOS 17: botón Refresh
(Button(intent:) → reloadAllTimelines, in-process) en medium/large, gated #available(iOS 17).
Reader read-only del AppGroup. Dialogs Siri refactorizados a String(localized:).
Localización: strings de #10 y de Siri 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 BackupService (export JSON v3 lossless a local + iCloud ubiquity, retención,
restore, premium-gated) ya existía de 1.1.0 pero solo era manual. Añadido lo automático:
- BackupService.runScheduledBackupIfNeeded() (config vía UserDefaults, gate por
backupsEnabled+premium, respeta frecuencia).
- Disparo en foreground (scenePhase .active) en background queue.
- Frecuencia Off/Diario/Semanal/Mensual + fecha del último backup en Settings
(SettingsViewModel.autoBackupFrequencyDays + updateAutoBackupFrequency).
- Strings en 7 idiomas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Goal ETA (#6): GoalProjection engine (CAGR mensual robusto de la evolución +
aportaciones, proyecta mes a mes → fecha estimada, estado ahead/on-track/behind/
unreachable, sparkline). GoalsView muestra 'Projected: <mes>', chip de ritmo con color,
fallback 'Add contributions to reach this'. GoalsViewModel.projection(for:) cacheado.
Insights (#7): InsightsEngine rule-based on-device (sin red/AI). Reglas: allocation vs
target, riesgo de concentración, mejor/peor categoría, diversificación, racha, YTD,
ganancias, forecast, proximidad a milestone. Priorizados por tono, top 4. PortfolioInsight
gana detail + tone. Surface en el Dashboard.
Localización: 19 claves nuevas en los 7 idiomas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
Reglas de borrado Account.sources/goals y Category.sources: Cascade -> Nullify.
Así un borrado de padre (local o propagado por CloudKit al importar) NUNCA arrasa
hijos — elimina de raíz el mecanismo del desastre del build 68. Además es mejor UX
(borrar una categoría ya no borra todas sus sources+snapshots).
CoreDataStack.mergeDefaultDuplicates(): fusiona los Account 'Default' duplicados y
las categorías duplicadas por nombre en un ganador DETERMINISTA (el registro con el
UUID estable si existe, si no earliest createdAt / menor UUID → todos los dispositivos
convergen). Reasigna sources/goals al ganador y borra los perdedores (seguro por Nullify).
Llamado en la carga inicial y tras cada import remoto.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
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