From 0b4796a707c81ebee46434c7acaa08ccb9ab8b3c Mon Sep 17 00:00:00 2001 From: Strycher Date: Sat, 11 Jul 2026 11:20:22 -0400 Subject: [PATCH] feat(#51): queue-sync file log + parked-drain re-run logging + manual force-drain action --- lib/connector/meshcore_connector.dart | 10 ++++-- lib/screens/settings_screen.dart | 18 ++++++++++ lib/services/queue_sync_file_log_io.dart | 40 ++++++++++++++++++++++ lib/services/queue_sync_file_log_stub.dart | 3 ++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 lib/services/queue_sync_file_log_io.dart create mode 100644 lib/services/queue_sync_file_log_stub.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2a016a9..988abc8 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -32,6 +32,8 @@ import '../services/background_service.dart'; import '../services/timeout_prediction_service.dart'; import '../services/translation_service.dart'; import '../services/notification_service.dart'; +import '../services/queue_sync_file_log_stub.dart' + if (dart.library.io) '../services/queue_sync_file_log_io.dart'; import 'meshcore_connector_usb.dart'; import 'meshcore_connector_tcp.dart'; import '../storage/channel_message_store.dart'; @@ -3547,11 +3549,13 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildSetDeviceTimeFrame(now)); } - // Mirror queue-sync events to the in-app App Debug Log (plus the console) so - // a missed/stuck drain is diagnosable from the device. See #51. + // Mirror queue-sync events to an always-on file sink (survives the app being + // closed during a drain), the in-app App Debug Log (when enabled), and the + // console, so a missed/stuck drain stays diagnosable. See #51. void _logQueueSync(String msg) { debugPrint('[QueueSync] $msg'); _appDebugLogService?.info(msg, tag: 'QueueSync'); + unawaited(appendQueueSyncLine(msg)); } Future syncQueuedMessages({bool force = false}) async { @@ -4064,6 +4068,7 @@ class MeshCoreConnector extends ChangeNotifier { if (_deferQueuedContactMessagesUntilContacts) { unawaited(_processDeferredQueuedContactMessages()); } else if (_pendingQueueSync) { + _logQueueSync('post-channel-frame: firing parked queue drain'); _pendingQueueSync = false; unawaited(syncQueuedMessages(force: true)); } @@ -4424,6 +4429,7 @@ class MeshCoreConnector extends ChangeNotifier { _deferQueuedContactMessagesUntilContacts = false; notifyListeners(); if (_pendingQueueSync && isConnected) { + _logQueueSync('post-deferred-contacts: firing parked queue drain'); _pendingQueueSync = false; unawaited(syncQueuedMessages(force: true)); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4563adc..49dafbe 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -605,6 +605,24 @@ class _SettingsScreenState extends State { ); }, ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.sync_problem_outlined), + title: const Text('Sync queued messages now'), + subtitle: const Text( + 'Force-pull messages the radio is holding — diagnostic for #51 ' + '(radio reports a count but the app shows none). Logs the flow ' + 'for diagnostics.', + ), + trailing: const Icon(Icons.download), + onTap: () { + context.read().syncQueuedMessages(force: true); + showDismissibleSnackBar( + context, + content: const Text('Requested queued-message drain (force)'), + ); + }, + ), ], ), ); diff --git a/lib/services/queue_sync_file_log_io.dart b/lib/services/queue_sync_file_log_io.dart new file mode 100644 index 0000000..4dd2d8d --- /dev/null +++ b/lib/services/queue_sync_file_log_io.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; + +// Always-on file sink for queue-sync diagnostics, independent of the in-app App +// Debug Log toggle (off by default, and its in-memory buffer is lost when the +// app is closed during a drain). Lands at +// /offband-queuesync.log. See #51. +// +// The IOSink is intentionally held open for the app's lifetime (append-only, +// low-frequency) rather than open/close per line; the OS closes the handle on +// process exit. `_sinkFuture ??= _openSink()` assigns synchronously before the +// await, so concurrent callers share one sink — no double-open on the event +// loop. + +Future? _sinkFuture; + +Future _openSink() async { + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/offband-queuesync.log'); + debugPrint('[QueueSync] file sink: ${file.path}'); + final sink = file.openWrite(mode: FileMode.append); + sink.writeln( + '\n===== offband queue-sync session ' + '${DateTime.now().toIso8601String()} =====', + ); + return sink; +} + +Future appendQueueSyncLine(String msg) async { + try { + final sink = await (_sinkFuture ??= _openSink()); + sink.writeln('${DateTime.now().toIso8601String()} $msg'); + await sink.flush(); + } catch (e) { + _sinkFuture = null; // allow re-init on the next event + debugPrint('[QueueSync] file sink error: $e'); + } +} diff --git a/lib/services/queue_sync_file_log_stub.dart b/lib/services/queue_sync_file_log_stub.dart new file mode 100644 index 0000000..dba98e2 --- /dev/null +++ b/lib/services/queue_sync_file_log_stub.dart @@ -0,0 +1,3 @@ +// Web/default stub: no local filesystem, so queue-sync diagnostics fall back to +// the in-app App Debug Log only. See #51. +Future appendQueueSyncLine(String msg) async {}