@@ -302,7 +302,9 @@ enum BackgroundSyncService {
302302 ) async -> SyncResult {
303303 logger. info ( " Background sync starting (reason= \( reason) ) " )
304304 trace ( " Starting background sync (reason= \( reason) , maxDuration= \( Int ( maxDuration) ) s). " )
305+ let syncStartedAt = Date ( )
305306 var telemetryEventCursor = latestBridgeEventID ( )
307+ let syncStartEventCursor = telemetryEventCursor
306308
307309 // Silent pushes arrive after iOS has suspended the process. The Go
308310 // bridge's cached state still reports isRunning=true, but the TCP
@@ -336,7 +338,9 @@ enum BackgroundSyncService {
336338 return completeSync (
337339 reason: reason,
338340 result: . noBookmarkAccess,
339- detail: L10n . tr ( " No security-scoped bookmark access was available. " )
341+ detail: L10n . tr ( " No security-scoped bookmark access was available. " ) ,
342+ startedAt: syncStartedAt,
343+ initialEventCursor: syncStartEventCursor
340344 )
341345 }
342346 trace ( " Restored bookmark access for \( managedURLs. count) URL(s). " )
@@ -352,7 +356,9 @@ enum BackgroundSyncService {
352356 return completeSync (
353357 reason: reason,
354358 result: . bridgeStartFailed,
355- detail: err
359+ detail: err,
360+ startedAt: syncStartedAt,
361+ initialEventCursor: syncStartEventCursor
356362 )
357363 }
358364 trace ( " Bridge start requested for background-owned sync. " )
@@ -368,7 +374,9 @@ enum BackgroundSyncService {
368374 return completeSync (
369375 reason: reason,
370376 result: . noFoldersConfigured,
371- detail: L10n . tr ( " No folders were available for background sync. " )
377+ detail: L10n . tr ( " No folders were available for background sync. " ) ,
378+ startedAt: syncStartedAt,
379+ initialEventCursor: syncStartEventCursor
372380 )
373381 }
374382
@@ -397,7 +405,9 @@ enum BackgroundSyncService {
397405 return completeSync (
398406 reason: reason,
399407 result: . bridgeStartFailed,
400- detail: restart. errorDetail ?? L10n . tr ( " Forced silent-push restart failed. " )
408+ detail: restart. errorDetail ?? L10n . tr ( " Forced silent-push restart failed. " ) ,
409+ startedAt: syncStartedAt,
410+ initialEventCursor: syncStartEventCursor
401411 )
402412 }
403413
@@ -423,7 +433,9 @@ enum BackgroundSyncService {
423433 return completeSync (
424434 reason: reason,
425435 result: . noFoldersConfigured,
426- detail: L10n . tr ( " No folders were available after forced silent-push restart. " )
436+ detail: L10n . tr ( " No folders were available after forced silent-push restart. " ) ,
437+ startedAt: syncStartedAt,
438+ initialEventCursor: syncStartEventCursor
427439 )
428440 }
429441
@@ -443,7 +455,13 @@ enum BackgroundSyncService {
443455 if ownsLifecycle {
444456 cleanupBackgroundManaged ( managedURLs)
445457 }
446- return completeSync ( reason: reason, result: . alreadyIdle, detail: nil )
458+ return completeSync (
459+ reason: reason,
460+ result: . alreadyIdle,
461+ detail: nil ,
462+ startedAt: syncStartedAt,
463+ initialEventCursor: syncStartEventCursor
464+ )
447465 }
448466
449467 let deadline = Date ( timeIntervalSinceNow: maxDuration)
@@ -495,7 +513,13 @@ enum BackgroundSyncService {
495513 result = . notIdleBeforeDeadline
496514 detail = L10n . fmt ( " Sync did not reach idle before %ds deadline. " , Int ( maxDuration) )
497515 }
498- return completeSync ( reason: reason, result: result, detail: detail)
516+ return completeSync (
517+ reason: reason,
518+ result: result,
519+ detail: detail,
520+ startedAt: syncStartedAt,
521+ initialEventCursor: syncStartEventCursor
522+ )
499523 }
500524
501525 // MARK: - BGAppRefreshTask Handler
@@ -603,7 +627,9 @@ enum BackgroundSyncService {
603627 private static func completeSync(
604628 reason: String ,
605629 result: SyncResult ,
606- detail: String ?
630+ detail: String ? ,
631+ startedAt: Date ,
632+ initialEventCursor: Int
607633 ) -> SyncResult {
608634 let outcome = SyncOutcome (
609635 timestamp: Date ( ) ,
@@ -612,6 +638,15 @@ enum BackgroundSyncService {
612638 detail: detail
613639 )
614640 persistSyncOutcome ( outcome)
641+ WidgetSnapshotStore . write (
642+ snapshot: WidgetSnapshotStore . Snapshot (
643+ lastSyncTime: WidgetSnapshotStore . iso8601String ( from: outcome. timestamp) ,
644+ lastSyncDuration: max ( 0 , outcome. timestamp. timeIntervalSince ( startedAt) ) ,
645+ status: result. isSuccessful ? " idle " : " error " ,
646+ filesSynced: syncedFileCount ( since: initialEventCursor) ,
647+ folderCount: currentFolderCount ( )
648+ )
649+ )
615650 if let detail, !detail. isEmpty {
616651 trace ( " Completed with result= \( result. rawValue) : \( detail) " )
617652 } else {
@@ -679,6 +714,26 @@ enum BackgroundSyncService {
679714 return total / Double( folders. count)
680715 }
681716
717+ private static func currentFolderCount( ) -> Int {
718+ let json = SyncBridgeService . getFoldersJSON ( )
719+ guard let data = json. data ( using: . utf8) ,
720+ let folders = try ? JSONDecoder ( ) . decode ( [ FolderStub ] . self, from: data) else {
721+ return 0
722+ }
723+ return folders. count
724+ }
725+
726+ private static func syncedFileCount( since lastEventID: Int ) -> Int {
727+ let events = decodeBridgeEvents ( from: SyncBridgeService . getEventsSince ( lastID: lastEventID) )
728+ return events. reduce ( into: 0 ) { count, event in
729+ guard event. type == " ItemFinished " else { return }
730+ let error = event. data ? [ " error " ] ?? " "
731+ if error. isEmpty {
732+ count += 1
733+ }
734+ }
735+ }
736+
682737 private static func notifyConflictsIfAny( ) async {
683738 let json = SyncBridgeService . getFoldersJSON ( )
684739 guard let data = json. data ( using: . utf8) ,
0 commit comments