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
This commit is contained in:
alexandrev-tibco
2026-07-10 14:16:24 +02:00
parent b76b8f81f0
commit 18f2609d87
14 changed files with 288 additions and 76 deletions
@@ -389,8 +389,42 @@ struct ChartsContainerView: View {
ChartStat(label: "Bull case", value: lastPred.confidenceInterval.upper.compactCurrencyString, color: .positiveGreen),
ChartStat(label: "Bear case", value: lastPred.confidenceInterval.lower.compactCurrencyString, color: .negativeRed),
]
case .yearOverYear, .comparison, .simulator, .periodComparison:
// These charts carry their own comparison summaries inside the card.
case .comparison:
let series = viewModel.comparisonData
guard !series.isEmpty else { return [] }
// Rank sources by their latest value in the current display mode.
let ranked = series.compactMap { s -> (name: String, val: Double)? in
guard let last = s.points.last?.value else { return nil }
return (s.name, last)
}.sorted { $0.val > $1.val }
let unit = viewModel.comparisonDisplayMode == .absolute ? "" : "%"
func fmt(_ v: Double) -> String { unit.isEmpty ? Decimal(v).compactCurrencyString : String(format: "%+.1f%%", v) }
var stats: [ChartStat] = [ChartStat(label: "Series", value: "\(series.count)", color: .secondary)]
if let best = ranked.first {
stats.append(ChartStat(label: "Leader", value: "\(best.name) \(fmt(best.val))", color: .positiveGreen))
}
if ranked.count > 1, let worst = ranked.last {
stats.append(ChartStat(label: "Laggard", value: "\(worst.name) \(fmt(worst.val))", color: .negativeRed))
}
return stats
case .periodComparison:
let series = viewModel.periodComparisonData
guard !series.isEmpty else { return [] }
return series.map { s in
let end = s.points.last?.returnPct ?? 0
return ChartStat(label: s.label, value: String(format: "%+.1f%%", end),
color: end >= 0 ? .positiveGreen : .negativeRed)
}
case .yearOverYear:
let years = viewModel.yearOverYearData.filter { viewModel.yoySelectedYears.contains($0.year) }
guard !years.isEmpty else { return [] }
return years.map { ys in
let end = ys.values.compactMap { $0.isNaN ? nil : $0 }.last ?? 0
return ChartStat(label: "\(ys.year)", value: String(format: "%+.1f%%", end),
color: end >= 0 ? .positiveGreen : .negativeRed)
}
case .simulator:
// The simulator carries its own live sliders and comparison inline.
return []
}
}