Compare commits

...
Author SHA1 Message Date
Thomas SharedInbox 551ac67daa fix: move sync health report to its own row below each account tile (#311)
The sync health widget was inside ListTile.subtitle, which caused it to
be squeezed to near-zero width by the trailing Row (status icon + menu).
Move it to a full-width Padding row below the ListTile so it can display
properly without layout pressure from the trailing widget.
2026-05-28 06:49:12 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 91c86d72ef fix: check issue state not labels in merged-PR catch-up
Closed issues can still have labels; the old `if not labels` guard
never fired for issue #272, so the loop retried the no-op close every
minute and never reached the ToPlan/Ready issue logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 06:49:12 +02:00
3 changed files with 102 additions and 73 deletions
+73 -70
View File
@@ -120,15 +120,76 @@ class _AccountTile extends ConsumerWidget {
final health = ref.watch(syncHealthProvider(account.id)); final health = ref.watch(syncHealthProvider(account.id));
final typeLabel = account.type == AccountType.jmap ? 'JMAP' : 'IMAP'; final typeLabel = account.type == AccountType.jmap ? 'JMAP' : 'IMAP';
return ListTile( return Column(
leading: const Icon(Icons.account_circle), crossAxisAlignment: CrossAxisAlignment.start,
title: Text(account.displayName), children: [
subtitle: Column( ListTile(
crossAxisAlignment: CrossAxisAlignment.start, leading: const Icon(Icons.account_circle),
children: [ title: Text(account.displayName),
Text('${account.email}\n$typeLabel'), subtitle: Text('${account.email}\n$typeLabel'),
const SizedBox(height: 4), isThreeLine: true,
health.when( trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
status.when(
loading: () => const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
data: (_) =>
const Icon(Icons.check_circle, color: Colors.green),
error: (e, _) => Tooltip(
message: e.toString(),
child: const Icon(Icons.error_outline, color: Colors.red),
),
),
PopupMenuButton<_AccountAction>(
onSelected: (action) => _onAction(context, action),
itemBuilder: (_) => [
const PopupMenuItem(
value: _AccountAction.syncLog,
child: Text('Sync log'),
),
const PopupMenuItem(
value: _AccountAction.verifySync,
child: Text('Verify sync health'),
),
const PopupMenuItem(
value: _AccountAction.forceSync,
child: Text('Force full sync'),
),
const PopupMenuItem(
value: _AccountAction.edit,
child: Text('Edit'),
),
if (_sieveSupported(account))
const PopupMenuItem(
value: _AccountAction.emailFiltersRemote,
child: Text('Server email filters'),
),
const PopupMenuItem(
value: _AccountAction.emailFiltersLocal,
child: Text('Local email filters'),
),
const PopupMenuItem(
value: _AccountAction.send,
child: Text('Send accounts'),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: _AccountAction.delete,
child: Text('Delete'),
),
],
),
],
),
onTap: () => context.push('/accounts/${account.id}/mailboxes'),
),
Padding(
padding: const EdgeInsets.fromLTRB(72, 0, 16, 8),
child: health.when(
data: (h) { data: (h) {
if (h == null) return const Text('Sync health: Not verified yet'); if (h == null) return const Text('Sync health: Not verified yet');
final date = h.lastVerifiedAt.toLocal().toString().split('.')[0]; final date = h.lastVerifiedAt.toLocal().toString().split('.')[0];
@@ -141,7 +202,7 @@ class _AccountTile extends ConsumerWidget {
color: h.isHealthy ? Colors.green : Colors.orange, color: h.isHealthy ? Colors.green : Colors.orange,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Flexible( Expanded(
child: Text( child: Text(
h.isHealthy h.isHealthy
? 'Healthy' ? 'Healthy'
@@ -155,66 +216,8 @@ class _AccountTile extends ConsumerWidget {
loading: () => const Text('Sync health: checking...'), loading: () => const Text('Sync health: checking...'),
error: (e, _) => Text('Sync health error: $e'), error: (e, _) => Text('Sync health error: $e'),
), ),
], ),
), ],
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
status.when(
loading: () => const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
data: (_) => const Icon(Icons.check_circle, color: Colors.green),
error: (e, _) => Tooltip(
message: e.toString(),
child: const Icon(Icons.error_outline, color: Colors.red),
),
),
PopupMenuButton<_AccountAction>(
onSelected: (action) => _onAction(context, action),
itemBuilder: (_) => [
const PopupMenuItem(
value: _AccountAction.syncLog,
child: Text('Sync log'),
),
const PopupMenuItem(
value: _AccountAction.verifySync,
child: Text('Verify sync health'),
),
const PopupMenuItem(
value: _AccountAction.forceSync,
child: Text('Force full sync'),
),
const PopupMenuItem(
value: _AccountAction.edit,
child: Text('Edit'),
),
if (_sieveSupported(account))
const PopupMenuItem(
value: _AccountAction.emailFiltersRemote,
child: Text('Server email filters'),
),
const PopupMenuItem(
value: _AccountAction.emailFiltersLocal,
child: Text('Local email filters'),
),
const PopupMenuItem(
value: _AccountAction.send,
child: Text('Send accounts'),
),
const PopupMenuDivider(),
const PopupMenuItem(
value: _AccountAction.delete,
child: Text('Delete'),
),
],
),
],
),
onTap: () => context.push('/accounts/${account.id}/mailboxes'),
); );
} }
+5 -3
View File
@@ -912,9 +912,11 @@ def _run_loop() -> int:
if not m: if not m:
continue continue
issue_num = int(m.group(1)) issue_num = int(m.group(1))
labels = _get_issue_labels(issue_num) try:
if not labels: issue_data = _tea_get(f"/repos/{REPO}/issues/{issue_num}")
# Issue is likely already closed — skip. except RuntimeError:
continue
if issue_data.get("state") != "open":
continue continue
pr_number = pr["number"] pr_number = pr["number"]
print(f"Catch-up (merged PR): PR #{pr_number} for issue #{issue_num} was merged — closing.") print(f"Catch-up (merged PR): PR #{pr_number} for issue #{issue_num} was merged — closing.")
+24
View File
@@ -252,5 +252,29 @@ void main() {
expect(find.textContaining('flag mismatches: 1'), findsOneWidget); expect(find.textContaining('flag mismatches: 1'), findsOneWidget);
}, },
); );
testWidgets(
'sync health row is positioned below the account name row',
(tester) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts',
overrides: baseOverrides(
accounts: [kTestAccount],
syncHealth: SyncHealthRow(
accountId: kTestAccount.id,
lastVerifiedAt: DateTime(2024, 6),
isHealthy: true,
),
),
),
);
await tester.pumpAndSettle();
final namePos = tester.getTopLeft(find.text('Alice')).dy;
final healthPos = tester.getTopLeft(find.textContaining('Healthy')).dy;
expect(healthPos, greaterThan(namePos));
},
);
}); });
} }