Commit Graph

73 Commits

Author SHA1 Message Date
alexandrev-tibco 8daa80bb42 Firma manual de los targets del Apple Watch para App Store
Los dos targets de watchOS seguían en firma automática y el export no tenía
sus perfiles, así que la build 88 nunca llegó a subirse. Ahora:

- PortfolioJournalWatch y PortfolioJournalWatchWidget en Release con
  CODE_SIGN_STYLE = Manual y sus perfiles App Store recién creados en el
  portal ("Portfolio Journal Watch App Store" / "... Watch Widget App Store").
- CODE_SIGN_IDENTITY con el nombre completo "Apple Distribution: Alexandre
  Vazquez" en los cinco targets embebidos, en vez del genérico.
- ExportOptions.plist y el script de release con los CINCO bundle ids en
  provisioningProfiles (faltaban los dos del reloj).
- El script valida con `altool --validate-app` antes de subir, para no gastar
  un número de build en un IPA que la App Store va a rechazar.

Con la firma totalmente manual y los perfiles instalados en local, el archive
ya no necesita -allowProvisioningUpdates ni la API key, así que esquiva el bug
del bearer token de xcodebuild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-18 23:02:04 +02:00
alexandrev-tibco 7d2f605c16 quitar AdMob, Firebase Analytics y ATT de la app
"Portfolio Journal sin AdMob ni Google: es la única forma de que 'sin
rastreo' sea verdad, y el ingreso por anuncios es despreciable" — la ficha
de App Store prometía "sin analíticas, sin rastreo, sin venta de datos"
mientras la app enlazaba GoogleMobileAds y FirebaseAnalytics, pedía permiso
de rastreo al arrancar y mandaba el importe del saldo a GA4 en el evento
snapshot_added.

SDKs fuera del proyecto (project.pbxproj): paquetes firebase-ios-sdk y
swift-package-manager-google-mobile-ads, productos FirebaseCore,
FirebaseAnalytics, GoogleMobileAds y FirebaseCrashlytics, más la fase de
build "Upload dSYMs to Crashlytics". Package.resolved se queda sin nada que
resolver. Con la fase de script fuera, ENABLE_USER_SCRIPT_SANDBOXING vuelve
a YES en el target app (estaba en NO solo por Crashlytics).

Código borrado:
- Services/AdMobService.swift entero — con él se van el flujo UMP,
  BannerAdView, BannerAdCoordinator y la llamada a
  ATTrackingManager.requestTrackingAuthorization().
- Services/FirebaseService.swift entero y sus 40 llamadas. Se borra en vez
  de dejarse como capa vacía: una clase llamada FirebaseService en una app
  que presume de no llevar Firebase es exactamente la clase de detalle que
  vuelve a colarse en una auditoría dentro de un año.
- El banner y su safeAreaInset en ContentView (bannerInsetView): las cinco
  pestañas ya no reservan 50 pt al pie.
- La entrada "Manage Ad Consent" de Ajustes y el @EnvironmentObject
  adMobService de SettingsView, ContentView y PortfolioJournalApp.
- AppConstants: bloque AdMob, bannerAdHeight, adConsentObtained,
  Features.enableAnalytics.
- SettingsViewModel.toggleAnalytics y analyticsEnabled — el flag no estaba
  conectado a nada ni tenía control en la interfaz. El atributo
  AppSettings.enableAnalytics se queda en CoreData con un comentario: no
  vale la pena una migración y un deploy de esquema CloudKit por borrarlo.
- Premium ya no promete "sin anuncios": fuera PremiumFeature.noAds, la
  entrada de IAPService.premiumFeatures y paywallBenefits, y las claves
  feature_no_ads / paywall_benefit_noads de los 7 idiomas. No se toca ni el
  precio ni el producto.

Info.plist: fuera NSUserTrackingUsageDescription, GADApplicationIdentifier,
GADDelayAppMeasurementInit y los 65 SKAdNetworkItems. Borrados también
GoogleService-Info.plist y los scripts Scripts/analyze_ga4.py y
Scripts/analyze_crashlytics.py, que ya no tienen de dónde leer.

Closes #50

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-18 16:09:04 +02:00
alexandrev-tibco 8828a2aa0f Versión 1.7.0 (build 89): release notes iPhone Duo y DUO_STATUS
- MARKETING_VERSION 1.7.0 / CURRENT_PROJECT_VERSION 89 en todos los targets.
- Release notes en-US/en-GB/en-AU/en-CA y es-ES/es-MX con iPhone Duo como
  primer bullet; RELEASE_NOTES.md y CHANGELOG.md.
- DUO_STATUS.md: qué se adaptó, escenarios regular×regular, pendiente fase 2
  (Xcode 27.1) y cómo probarlo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 15:13:10 +02:00
alexandrev-tibco a8a9ad64f3 Xcode 27: compila sin warnings y migra API deprecadas de SwiftUI
- Imports de CoreData que faltaban (MemberImportVisibility) en AccountsView,
  ImportDataView, ChartsContainerView y AddSourceView.
- Aislamiento MainActor: IsolatedDefaultValues en el target de la app,
  LinearGradient @MainActor, WatchSyncMessage nonisolated, closures de
  ReviewPromptService/MonthlyCheckInStore/AppIntents.
- Warnings de valores sin usar (SnapshotGapDetector, DashboardLayoutStore,
  OnboardingView, GoalEditorView, CoreDataStack, SettingsViewModel).
- foregroundColor→foregroundStyle, cornerRadius→clipShape,
  navigationBarLeading/Trailing→topBarLeading/Trailing,
  Task.sleep(nanoseconds:)→sleep(for:), NavigationLink(isActive:)→
  navigationDestination(isPresented:).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 11:44:49 +02:00
alexandrev-tibco 464fbb2bad App de Apple Watch + 3 complicaciones (build 88) #48
App de watchOS de solo consulta: resumen de cartera (total, cambio del
mes y mayores posiciones), objetivos con su progreso y estado del
check-in con la racha. Toda la edición sigue en el iPhone.

Complicaciones (accessoryCircular / Rectangular / Inline / Corner):
valor total de la cartera, progreso del objetivo más cercano a cumplirse
y racha + estado del check-in.

Datos: los App Groups no se comparten entre iOS y watchOS, así que el
reloj no puede leer el store de CoreData que lee el widget de iOS. En su
lugar, WatchSyncService construye un WatchPortfolioSnapshot ligero y lo
envía como application context de WatchConnectivity, enganchado a
CoreDataStack.refreshWidgetData() — el mismo punto que refresca el widget
de iOS, así que cualquier repositorio que toque datos refresca el reloj.
WatchDataStore lo cachea en el App Group del watch (WatchSnapshotCache) y
recarga las timelines; la extensión de complicaciones solo lee ese caché.

Proyecto: dos targets nuevos (watchOS app + widget extension) con grupos
sincronizados de Xcode 16; Shared/ pertenece a la app de watch y se
comparte con la app de iOS y el widget mediante exception sets, el mismo
mecanismo que ya usaba el widget de iOS para el modelo de CoreData.
Scheme compartido PortfolioJournalWatch. Localización propia por target
en los 7 idiomas.

Verificado: compilan los tres targets, la app de watch queda embebida en
Watch/ con la extensión en PlugIns/, y en el par de simuladores
iPhone 17 Pro Max + Apple Watch Series 11 el reloj recibe y muestra el
snapshot enviado por el iPhone. 10 tests nuevos del payload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcDn5ccuRFV83q7xWWokBT
2026-09-13 22:05:31 +02:00
alexandrev-tibco d88cc59627 Goals por categoría: ámbito de categoría en objetivos (build 88) #46
Un Goal ya podía acotarse por cuenta; ahora también por categoría de
fuente, de modo que se puede fijar "100.000 € en Cash" y que el progreso
cuente solo las fuentes de esa categoría.

- CoreData: relación Goal.category ↔ Category.goals (opcional, Nullify)
- Goal.includes(_:)/relevantSources(from:)/scopeKey centralizan el filtro
  de ámbito (cuenta Y categoría) que antes estaba duplicado en
  GoalsViewModel, AddSourceView y NotificationService
- GoalEditorView: sección "Ámbito" con selector de categoría
- Chip de categoría en la lista de Goals y en la tarjeta del Dashboard
- ETA del Dashboard para goals con ámbito: usa la proyección del propio
  goal en vez de la evolución global de la cartera (que daría "objetivo
  alcanzado" en cuanto la cartera total superara el importe)
- GoalRepository e ImportService deduplican por (nombre, cuenta, categoría)
- Export/Import de goals incluye "category"
- Nuevas claves localizadas en los 7 idiomas

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcDn5ccuRFV83q7xWWokBT
2026-09-12 09:32:02 +02:00
alexandrev-tibco eaf0c8f93b Fix GA4: screen_view de Dashboard fuera de refreshData() (build 87) #44
logScreenView('Dashboard') estaba dentro de refreshData(), que se re-ejecuta con cada cambio de CoreData (incl. imports de iCloud, debounce 500ms) — 2 iPads con la app abierta generaban 1.400-2.100 views fantasma/día. Movido a DashboardView.onAppear (una vez por aparición), como el resto de pantallas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXtwQP6nYnbLFVqsHiTK5G
2026-09-10 06:34:21 +02:00
alexandrev-tibco 6dd755234a Fix widget: refrescar timeline al guardar Quick Update y al pasar a background (build 86) #42
- QuickUpdateView.saveAll hacía context.save() directo sin pasar por SnapshotRepository → nunca llamaba a refreshWidgetData() y el widget quedaba obsoleto hasta reabrir la app
- Red de seguridad: refreshWidgetData() también en scenePhase .background para cualquier otro camino de guardado directo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXtwQP6nYnbLFVqsHiTK5G
2026-09-01 16:19:52 +02:00
alexandrev-tibco 66dc854461 Fix instrumentación: snapshot_added también con valor inicial en AddSourceView (build 85) #39
El snapshot creado desde el valor inicial no registraba snapshot_added en Firebase — el funnel de activación quedaba infracontado justo tras el cambio de la 1.6.1 que empuja a ese camino.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXtwQP6nYnbLFVqsHiTK5G
2026-08-26 14:29:04 +02:00
alexandrev-tibco f2f54a5ca3 Activación: valor inicial como primer check-in + CTA en Quick Update vacío (build 84) #37
- AddSourceView: sección de valor reencuadrada como 'Valor de hoy' (primer check-in); si se omite, abre AddSnapshotView automáticamente en vez de dejar al usuario en el dashboard vacío
- QuickUpdateView: estado sin fuentes ahora tiene CTA 'Añade tu primera fuente' → .openAddFirstSource
- 3 claves nuevas localizadas en los 7 idiomas
- Bump a 1.6.1 (84)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VXtwQP6nYnbLFVqsHiTK5G
2026-08-23 12:37:58 +02:00
alexandrev-tibco 3b1f62cc26 Share cards compartibles: brand header grande, fecha, Dark/Light (build 83) #35
- Header de marca arriba, grande (logo BrandMark 48px + nombre title2 rounded),
  nunca se corta en Story (9:16 real 360x640, Post 4:5 360x450).
- Héroe = retorno % (privacy/branding-friendly), valor como apoyo.
- Fecha del último check-in bajo el nombre (share_as_of) para dar contexto a
  quien no conoce la app; DashboardViewModel.shareAsOfDate.
- Tema Dark + Light (ShareCardTheme) con selector en el sheet + preview vivo.
- Sparkline real (viewModel.evolutionData), ya se pasaba.
- CTA de descarga + QR con ct=snapshot_share (atribución App Analytics).
- L10n x7 (as_of, theme_dark/light, cta, hero, metrics).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
2026-08-07 09:45:10 +02:00
alexandrev-tibco 8084bc6a82 Share cards 2.0: dark panorama + privacidad + Story + atribución (build 82) #34
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
2026-08-06 23:09:59 +02:00
alexandrev-tibco 6e6f4c5489 Dashboard: quitar títulos duplicados en cards de gráficas (build 81) #33
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
2026-08-06 22:55:27 +02:00
alexandrev-tibco cf984dbf09 Quitar swipe entre charts + ventana del brush deslizable (build 80) #31 #32
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
2026-08-06 21:46:07 +02:00
alexandrev-tibco f1aeafccf6 Brush global de rango: filtro de datos para todos los charts + fix swipe (build 79)
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
2026-08-06 20:33:58 +02:00
alexandrev-tibco d941ecf27b Fix ITMS-90626: quitar 'Siri' de descripciones de App Intents + build 78
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
2026-08-06 20:11:58 +02:00
alexandrev-tibco 9c2a67853d Bump build 77 (TestFlight: charts workable)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
2026-08-06 18:07:23 +02:00
alexandrev-tibco eee0910680 Rama 1.6.0: dedup CloudKit-safe (WIP: StableID + UUIDs estables en defaults)
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
2026-07-25 12:04:04 +02:00
alexandrev-tibco e4a93a5d4a Bump 1.5.1 (build 76)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-25 11:47:53 +02:00
alexandrev-tibco 8d5f309f21 Bump build 75
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-24 09:36:51 +02:00
alexandrev-tibco 8d7cfc098b Bump build 74
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-23 20:13:22 +02:00
alexandrev-tibco b60e3041cb Bump build 73
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-23 19:51:27 +02:00
alexandrev-tibco fc05dfe2c6 Bump build 72
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-23 18:59:37 +02:00
alexandrev-tibco 2ea32b5494 Bump build 71
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-23 11:38:33 +02:00
alexandrev-tibco 57280ca3ca Bump build 70
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-23 11:02:57 +02:00
alexandrev-tibco d50926df76 Bump build 69 + cambios pendientes (scheme, AppShortcuts)
Build 69: primer build sin el auto-dedup destructivo. Base limpia para
recuperar los dispositivos vía TestFlight tras el incidente de build 68.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-22 19:58:24 +02:00
alexandrev-tibco 20b8a32ea3 Fix duplicados CloudKit cross-device: dedup por identidad lógica + bump build 68
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
2026-07-22 15:56:41 +02:00
alexandrev-tibco f34460d97a Diagnóstico iCloud: dump completo del árbol CKError (códigos hoja + record types) + sonda de integridad local
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
2026-07-21 18:00:22 +02:00
alexandrev-tibco e916218271 Bump build 66
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
2026-07-21 15:23:43 +02:00
alexandrev-tibco 32e1fb1c9c Bump build 65
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
2026-07-21 13:56:39 +02:00
alexandrev-tibco 1e6e3ef361 Re-engagement con KPIs reales (goal ETA, YoY, mes, milestone, racha) + rotación (build 64)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZFmGhbWzibhApev3C4554
2026-07-20 19:29:27 +02:00
alexandrev-tibco 41e2a22a64 Swipe fiable en card del Home y Charts: simultaneousGesture sobre la gráfica (build 63)
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
2026-07-11 10:50:52 +02:00
alexandrev-tibco d8bd625327 Swipe entre mini-charts en la card del Home (build 62)
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
2026-07-10 22:13:00 +02:00
alexandrev-tibco 7c6a29c821 OCR desde imagen del portapapeles + botón copiar diagnóstico iCloud (build 61)
- 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
2026-07-10 21:52:53 +02:00
alexandrev-tibco 6bdf2740c5 Feedback #3 y #4: pegar/OCR por campo en Quick Update + swipe entre charts (build 60)
#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
2026-07-10 21:37:21 +02:00
alexandrev-tibco 86c95a9e73 Feedback TestFlight build 58 (build 59): labels, needs-update, period selector, Home masonry
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
2026-07-10 20:36:15 +02:00
alexandrev-tibco 1abffdb7bb Feedback iPad: allocation grande + %, Home 2 columnas, YoY KPIs, achievements fix, iCloud calmado (build 58)
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
2026-07-10 14:48:06 +02:00
alexandrev-tibco 18f2609d87 Fixes UI iPad + diagnóstico iCloud (build 57)
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
2026-07-10 14:16:24 +02:00
alexandrev-tibco 4c0d2af564 Calm 2.0 Fase 4: TipKit para descubrimiento + aha post-onboarding (build 55)
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
2026-07-10 09:31:03 +02:00
alexandrev-tibco 08094483ed What's New versionado y acumulativo (build 54)
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
2026-07-10 08:06:49 +02:00
alexandrev-tibco ed737bc290 Calm 2.0 Fase 3: Home iPad columna única, Goals con anillos, Journal timeline, Sources polish (build 53)
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
2026-07-09 11:11:04 +02:00
alexandrev-tibco 7044cfd441 Calm 2.0 Fase 2: check-in guiado full-screen (build 52)
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
2026-07-09 10:51:11 +02:00
alexandrev-tibco be7119f4b9 Calm 2.0 Fase 1: sistema de diseño limpio + Home en 3 zonas (1.5.0 build 51)
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
2026-07-08 23:58:06 +02:00
alexandrev-tibco ff67ea1e71 Fix causa raíz de taps muertos en Charts iPad: AppBackground interceptaba toques (build 50)
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
2026-07-08 23:30:30 +02:00
alexandrev-tibco 7b8dec196d Charts 2.0 (build 49): sidebar nativa, filtros en toolbar, title menu en iPhone
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
2026-07-08 22:45:31 +02:00
alexandrev-tibco 19b1408c55 1.4.2 (build 48): OCR de screenshots en Share Extension
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
2026-07-08 15:59:54 +02:00
alexandrev-tibco a953af2da0 Firma manual de distribución en Release para app, widget y extension QuickUpdate
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
2026-07-07 14:39:17 +02:00
alexandrev-tibco cef5924f16 1.4.1 (build 47): labels en charts, Share Extension Quick Update y clipboard auto-advance
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
2026-07-04 07:40:30 +02:00
alexandrev-tibco 386f4ff265 Actualización sin cambiar de app: Share Extension "Quick Update" + clipboard auto-advance
El dolor: para actualizar cada source hay que saltar a la app del banco, copiar/
memorizar el valor y volver. iOS no permite ventanas flotantes editables (PiP es
solo vídeo), así que se resuelve con las dos vías sancionadas:

Share Extension (target nuevo PortfolioJournalQuickUpdate):
- Al compartir texto desde CUALQUIER app aparece "Portfolio Journal": mini-form
  sobre la app anfitriona con el importe pre-parseado del texto compartido y la
  siguiente source pendiente del mes preseleccionada. Guardar → sigues en el banco.
- Sin Core Data en la extensión: la app publica un espejo de sources activas en el
  App Group (SharedQuickUpdateSync.refreshMirror) y la extensión encola
  PendingQuickUpdate; la app los convierte en Snapshots reales al activarse
  (ingestPending). ExtSharedBridge duplica el contrato (keep-in-sync comentado).
- Parser de importes tolerante a locales (12.345,67 € / $1,234.56 / 1234,5).
- UI localizada en los 7 idiomas (lproj propios de la extensión).
- pbxproj: target app-extension completo (sync group, embed, dependency, configs)
  replicando el patrón del widget. Entitlements solo con App Group.

Clipboard auto-advance en Quick Update:
- Al volver a la app con un importe copiado: banner de un tap "Pegar X en <source>"
  que rellena la siguiente source vacía y avanza el foco a la siguiente.
- Badge NEXT en la fila activa; no repite sugerencias del mismo clipboard.

Script de upload: -allowProvisioningUpdates + API key también en el archive para
que el bundle id nuevo de la extensión se registre automáticamente.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
2026-07-03 23:21:55 +02:00
alexandrev-tibco 7c92ea36c0 1.4.1 (build 46): fix cambio de Chart Type roto por el cache de snapshots
El fetch del cache dependía del timeRange activo (monthsLimit = timeRange.months),
así que al reutilizarlo entre chart types/rangos (optimización del build 45) los
charts recibían historia truncada — Year vs Year o "All" con solo 12 meses, charts
vacíos o incompletos al cambiar de tipo.

Fix: el cache siempre guarda la historia completa (maxHistoryMonths) y el recorte
por rango se hace en memoria (el filtro por cutoffDate ya existía justo después).
Bonus: hiddenHistoryMonths ahora se calcula contra la ventana completa (teaser más
preciso).

Verificado en iPad Pro 13" simulador: Evolution 12M → All → Compare →
Period vs Period → Evolution, todos renderizan con datos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
2026-07-03 16:35:42 +02:00