diff --git a/docs/progress.md b/docs/progress.md index d5a72ab..f7f5ed7 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -2,6 +2,21 @@ Updated: 2026-02-15 +## Latest Milestone (2026-02-15): Sync Rejection Payload Inspection + +Improved conflict handling UX in Sync view: + +- Updated `lib/features/sync/sync_view.dart`: + - each rejected mutation now supports payload inspect/expand + - shows formatted local envelope payload JSON for failed mutations when + available + - keeps requeue/dismiss actions for operator control + +Validation for this milestone: + +- `flutter analyze` -> pass +- `flutter test` -> pass + ## Latest Milestone (2026-02-15): Reachability-Gated Auto Sync Added connectivity-aware gating to background sync auto-triggers: @@ -157,7 +172,8 @@ Validation for this milestone: - replace no-op scheduler with platform local notifications implementation for Android/iOS/macOS/Windows/Linux/Web-safe fallback. 2. Conflict resolution UX: - - reject/requeue is implemented; still missing guided per-entity repair UI. + - reject/requeue + payload inspect are implemented; future enhancement is + direct deep-link navigation to entity edit screens from Sync view. 3. Sync trigger maturity: - implemented via reachability gating; future improvement is explicit OS connectivity event subscription. diff --git a/lib/features/sync/sync_view.dart b/lib/features/sync/sync_view.dart index 1c38ac4..41bd3ac 100644 --- a/lib/features/sync/sync_view.dart +++ b/lib/features/sync/sync_view.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; @@ -17,6 +19,7 @@ class SyncView extends ConsumerStatefulWidget { class _SyncViewState extends ConsumerState { bool _busy = false; String _status = 'Ready. Local-first mode keeps app usable without sync.'; + final Set _expandedRejections = {}; @override Widget build(BuildContext context) { @@ -185,18 +188,28 @@ class _SyncViewState extends ConsumerState { ], ), const SizedBox(height: 8), - ...state.lastRejected.map( - (MutationRejection rejection) => Padding( + ...state.lastRejected.map((MutationRejection rejection) { + final ChangeEnvelope? matched = _findRejectedChange( + state.lastRejectedChanges, + rejection.clientMutationId, + ); + final bool expanded = _expandedRejections.contains( + rejection.clientMutationId, + ); + return Padding( padding: const EdgeInsets.only(bottom: 10), child: _rejectionTile( rejection, - matched: _findRejectedChange( - state.lastRejectedChanges, - rejection.clientMutationId, - ), + matched: matched, + expanded: expanded, + onToggleExpanded: matched?.payload == null + ? null + : () => _toggleRejectionExpanded( + rejection.clientMutationId, + ), ), - ), - ), + ); + }), ], ), ), @@ -258,6 +271,8 @@ class _SyncViewState extends ConsumerState { Widget _rejectionTile( MutationRejection rejection, { required ChangeEnvelope? matched, + required bool expanded, + required VoidCallback? onToggleExpanded, }) { return Container( width: double.infinity, @@ -300,6 +315,17 @@ class _SyncViewState extends ConsumerState { color: AppTheme.textSecondary, ), ), + if (onToggleExpanded != null) + IconButton( + onPressed: onToggleExpanded, + tooltip: expanded ? 'Hide payload' : 'Show payload', + icon: Icon( + expanded + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + color: AppTheme.textSecondary, + ), + ), ], ), const SizedBox(height: 6), @@ -307,6 +333,21 @@ class _SyncViewState extends ConsumerState { rejection.message, style: Theme.of(context).textTheme.bodyMedium, ), + if (expanded && matched?.payload != null) ...[ + const SizedBox(height: 10), + Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFFF3F8FB), + borderRadius: BorderRadius.circular(10), + ), + child: SelectableText( + const JsonEncoder.withIndent(' ').convert(matched!.payload), + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], ], ), ); @@ -435,4 +476,14 @@ class _SyncViewState extends ConsumerState { } } } + + void _toggleRejectionExpanded(String clientMutationId) { + setState(() { + if (_expandedRejections.contains(clientMutationId)) { + _expandedRejections.remove(clientMutationId); + } else { + _expandedRejections.add(clientMutationId); + } + }); + } }