feat: surface per-calendar sync errors from /api/caldav/events
The server now reports per-calendar sync failures (e.g. expired credentials) alongside an otherwise-successful events response, via an "errors" array. Previously such failures were silently swallowed, so a calendar could appear enabled while showing zero events with no explanation. - Decode the new "errors" field into SyncError (source/name/message), following the existing JSONSerialization + from(json:) convention. - CalendarStore.syncErrors captures these on successful personal fetches (loadEvents, prefetchBackground); left untouched on a hard fetch failure so it isn't conflated with lastError. Group overlay fetches never populate it (the endpoint doesn't apply there). - CalendarHostView shows a second red warning banner alongside the existing lastError banner, reusing the same visual style.
This commit is contained in:
@@ -284,10 +284,14 @@ struct CalendarHostView: View {
|
||||
.accessibilityLabel(L10n.t("nav.menu", appLang))
|
||||
}
|
||||
|
||||
// MARK: – Error banner
|
||||
// MARK: – Error banners
|
||||
|
||||
// `lastError` (whole fetch failed) and `syncErrors` (fetch succeeded, but
|
||||
// individual calendars didn't sync) are independent conditions and can
|
||||
// both be shown at once.
|
||||
@ViewBuilder private var errorBanner: some View {
|
||||
if let err = store.lastError { errorBannerView(err) }
|
||||
if !store.syncErrors.isEmpty { syncErrorBannerView(store.syncErrors) }
|
||||
}
|
||||
|
||||
private func errorBannerView(_ err: String) -> some View {
|
||||
@@ -303,6 +307,23 @@ struct CalendarHostView: View {
|
||||
.background(Color.red.opacity(0.85))
|
||||
}
|
||||
|
||||
/// One or more calendars didn't sync on the last fetch (e.g. expired
|
||||
/// credentials) even though they're still enabled. Same visual language
|
||||
/// as `errorBannerView`, joined into a single compact banner.
|
||||
private func syncErrorBannerView(_ errors: [SyncError]) -> some View {
|
||||
let text = errors.map { "\($0.source) (\($0.name)): \($0.message)" }.joined(separator: "\n")
|
||||
return HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.yellow)
|
||||
Text(text).font(.caption).foregroundStyle(.white).lineLimit(errors.count + 1)
|
||||
Spacer()
|
||||
Button { Task { await onNavigate() } } label: {
|
||||
Image(systemName: "arrow.clockwise").foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.vertical, 8)
|
||||
.background(Color.red.opacity(0.85))
|
||||
}
|
||||
|
||||
// MARK: – Calendar content (with swipe)
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
Reference in New Issue
Block a user