Skip to content
15 changes: 8 additions & 7 deletions lib/models/noise_floor_session.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import '../utils/ping_colors.dart';

part 'noise_floor_session.g.dart';

Expand Down Expand Up @@ -100,13 +101,13 @@ class PingEventMarker extends HiveObject {

/// Get the color for this event type
Color get color => switch (type) {
PingEventType.txSuccess => Colors.green,
PingEventType.txFail => Colors.red,
PingEventType.rx => Colors.blue,
PingEventType.discSuccess => Colors.purple,
PingEventType.discFail => Colors.grey,
PingEventType.traceSuccess => Colors.cyan,
PingEventType.traceFail => Colors.grey,
PingEventType.txSuccess => PingColors.txSuccess,
PingEventType.txFail => PingColors.txFail,
PingEventType.rx => PingColors.rx,
PingEventType.discSuccess => PingColors.discSuccess,
PingEventType.discFail => PingColors.discFail,
PingEventType.traceSuccess => PingColors.traceSuccess,
PingEventType.traceFail => PingColors.noResponse,
};

/// Get a display label for this event type
Expand Down
22 changes: 22 additions & 0 deletions lib/providers/app_state_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {

// Tile refresh after upload
int _overlayCacheBust = 0;
int _mapDataRevision = 0;
Timer? _tileRefreshTimer;

// Auth type from API response (API, Mesh, Manual)
Expand Down Expand Up @@ -441,6 +442,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
String? get zoneName => _currentZone?['name'] as String?;
String? get zoneCode => _currentZone?['code'] as String?;
int get overlayCacheBust => _overlayCacheBust;
int get mapDataRevision => _mapDataRevision;
int? get zoneSlotsAvailable => _currentZone?['slots_available'] as int?;
int? get zoneSlotsMax => _currentZone?['slots_max'] as int?;
String? get nearestZoneName => _nearestZone?['name'] as String?;
Expand Down Expand Up @@ -519,6 +521,16 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
int get offlinePingCount => _apiQueueService.offlinePingCount;
OfflineSessionService get offlineSessionService => _offlineSessionService;

/// Bumps the selected map revision so widgets depending on marker/log data
/// rebuild even when the underlying collections are mutated in place.
///
/// Call this whenever map-visible data changes without replacing the lists;
/// otherwise the map can keep showing stale markers until some unrelated
/// state change triggers a rebuild.
void _markMapDataChanged() {
_mapDataRevision++;
}

/// Distance in meters from last TX ping position (like wardrive.js)
double? get distanceFromLastPing {
if (_currentPosition == null) return null;
Expand Down Expand Up @@ -1319,6 +1331,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
_pingService!.onTxPing = (ping) {
_txPings.add(ping);
if (_txPings.length > _maxMapPins) _txPings.removeAt(0);
_markMapDataChanged();

// Add TX log entry (power in watts from preferences)
_txLogEntries.add(TxLogEntry(
Expand All @@ -1336,6 +1349,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
_pingService!.onRxPing = (ping) {
_rxPings.add(ping);
if (_rxPings.length > _maxMapPins) _rxPings.removeAt(0);
_markMapDataChanged();

// Add RX log entry
_rxLogEntries.add(RxLogEntry(
Expand Down Expand Up @@ -1818,6 +1832,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
);
_rxPings.add(rxPing);
if (_rxPings.length > _maxMapPins) _rxPings.removeAt(0);
_markMapDataChanged();
_currentBatchRepeaters.add(repeaterKey);

// Increment RX count immediately when pin is created (not on batch flush)
Expand Down Expand Up @@ -1889,6 +1904,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
snr: entry.snr ?? existingPin.snr, // UPDATE to best SNR from batch
rssi: entry.rssi ?? existingPin.rssi,
);
_markMapDataChanged();
debugLog('[APP] Updated RX pin SNR for repeater=${entry.repeaterId}: '
'${existingPin.snr.toStringAsFixed(2)} -> ${entry.snr?.toStringAsFixed(2) ?? 'null'}');
} else {
Expand All @@ -1907,6 +1923,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
);
_rxPings.add(newRxPing);
if (_rxPings.length > _maxMapPins) _rxPings.removeAt(0);
_markMapDataChanged();
debugLog('[APP] Created FALLBACK RX pin for repeater=${entry.repeaterId} '
'at ${entry.lat.toStringAsFixed(5)},${entry.lon.toStringAsFixed(5)}');
}
Expand Down Expand Up @@ -2825,6 +2842,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
void clearPings() {
_txPings.clear();
_rxPings.clear();
_markMapDataChanged();
_clearOverlayState();
_pingService?.resetStats();
notifyListeners();
Expand All @@ -2837,6 +2855,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
_discLogEntries.clear();
_traceLogEntries.clear();
_errorLogEntries.clear();
_markMapDataChanged();
_clearOverlayState();
notifyListeners();
}
Expand All @@ -2847,6 +2866,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
if (_discLogEntries.length > _maxLogEntries) {
_discLogEntries.removeLast();
}
_markMapDataChanged();
debugLog('[APP] Discovery log entry added: ${entry.nodeCount} nodes discovered');
notifyListeners();
}
Expand Down Expand Up @@ -4187,6 +4207,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {

// Clear repeaters when exiting zone
_repeaters = [];
_markMapDataChanged();
_repeatersLoaded = false;
_repeatersLoadedForIata = null;
}
Expand Down Expand Up @@ -4275,6 +4296,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
try {
final fetchedRepeaters = await _apiService.fetchRepeaters(iata);
_repeaters = fetchedRepeaters;
_markMapDataChanged();
_repeatersLoaded = true;
_repeatersLoadedForIata = iata;
debugLog('[MAP] Loaded ${_repeaters.length} repeaters for zone $iata');
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/connection_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ class _ConnectionScreenState extends State<ConnectionScreen> with WidgetsBinding
}

// Portrait: compact vertical layout (bottom bar provided by _buildBody)
return Padding(
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
Expand Down
17 changes: 9 additions & 8 deletions lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import '../models/connection_state.dart';
import '../providers/app_state_provider.dart';
import '../utils/distance_formatter.dart';
import '../utils/ping_colors.dart';
import '../widgets/connection_panel.dart';
import '../widgets/map_widget.dart';
import '../widgets/ping_controls.dart';
Expand Down Expand Up @@ -155,31 +156,31 @@ class _HomeScreenState extends State<HomeScreen> {
_buildAppBarStatChip(
Icons.arrow_upward,
appState.pingStats.txCount,
Colors.green,
PingColors.txSuccess,
onTap: withTapHandlers ? () => _showInfoPopup('tx', appState) : null,
),
const SizedBox(width: 8),
// RX count
_buildAppBarStatChip(
Icons.arrow_downward,
appState.pingStats.rxCount,
Colors.blue,
PingColors.rx,
onTap: withTapHandlers ? () => _showInfoPopup('rx', appState) : null,
),
const SizedBox(width: 8),
// DISC count
_buildAppBarStatChip(
Icons.radar,
appState.pingStats.discCount,
const Color(0xFF7B68EE),
PingColors.discSuccess,
onTap: withTapHandlers ? () => _showInfoPopup('disc', appState) : null,
),
const SizedBox(width: 8),
// Trace count
_buildAppBarStatChip(
Icons.route,
appState.pingStats.traceCount,
Colors.cyan,
PingColors.traceSuccess,
onTap: withTapHandlers ? () => _showInfoPopup('trace', appState) : null,
),
const SizedBox(width: 8),
Expand Down Expand Up @@ -331,16 +332,16 @@ class _HomeScreenState extends State<HomeScreen> {
return ('Locating...', 'Acquiring GPS signal and checking your zone status.', Icons.gps_not_fixed, Colors.blue);

case 'tx':
return ('TX Packets', 'TX packets that have been sent out. These are messages to the #wardriving channel.', Icons.arrow_upward, Colors.green);
return ('TX Packets', 'TX packets that have been sent out. These are messages to the #wardriving channel.', Icons.arrow_upward, PingColors.txSuccess);

case 'rx':
return ('RX Packets', 'RX packets that we have heard from the mesh. These were not initiated by us.', Icons.arrow_downward, Colors.blue);
return ('RX Packets', 'RX packets that we have heard from the mesh. These were not initiated by us.', Icons.arrow_downward, PingColors.rx);

case 'disc':
return ('Discovery Requests', 'Discovery request packets we have sent out.', Icons.radar, const Color(0xFF7B68EE));
return ('Discovery Requests', 'Discovery request packets we have sent out.', Icons.radar, PingColors.discSuccess);

case 'trace':
return ('Trace Responses', 'Trace path requests that received a response from the target repeater.', Icons.route, Colors.cyan);
return ('Trace Responses', 'Trace path requests that received a response from the target repeater.', Icons.route, PingColors.traceSuccess);

case 'upload':
return ('Uploaded', 'Pings sent to MeshMapper servers. Your data helps build the community coverage map!', Icons.cloud_done, Colors.teal);
Expand Down
19 changes: 10 additions & 9 deletions lib/screens/log_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import '../models/log_entry.dart';
import '../models/repeater.dart';
import '../providers/app_state_provider.dart';
import '../utils/ping_colors.dart';
import '../widgets/repeater_id_chip.dart';

/// Log screen with two tabs: All Pings (unified TX+RX+DISC+TRC) and Errors
Expand Down Expand Up @@ -434,13 +435,13 @@ class _AllPingsTabState extends State<_AllPingsTab> {
child: IntrinsicHeight(
child: Row(
children: [
_buildFilterSegment(PingLogType.tx, 'TX', widget.txCount, Colors.green, isFirst: true),
_buildFilterSegment(PingLogType.tx, 'TX', widget.txCount, PingColors.txSuccess, isFirst: true),
_segmentDivider(context),
_buildFilterSegment(PingLogType.rx, 'RX', widget.rxCount, Colors.blue),
_buildFilterSegment(PingLogType.rx, 'RX', widget.rxCount, PingColors.rx),
_segmentDivider(context),
_buildFilterSegment(PingLogType.disc, 'DISC', widget.discCount, const Color(0xFF7B68EE)),
_buildFilterSegment(PingLogType.disc, 'DISC', widget.discCount, PingColors.discSuccess),
_segmentDivider(context),
_buildFilterSegment(PingLogType.trace, 'TRC', widget.traceCount, Colors.cyan, isLast: true),
_buildFilterSegment(PingLogType.trace, 'TRC', widget.traceCount, PingColors.traceSuccess, isLast: true),
],
),
),
Expand Down Expand Up @@ -548,10 +549,10 @@ class _AllPingsTabState extends State<_AllPingsTab> {

static Widget _buildTypeBadge(PingLogType type) {
final (label, color) = switch (type) {
PingLogType.tx => ('TX', Colors.green),
PingLogType.rx => ('RX', Colors.blue),
PingLogType.disc => ('DISC', const Color(0xFF7B68EE)),
PingLogType.trace => ('TRC', Colors.cyan),
PingLogType.tx => ('TX', PingColors.txSuccess),
PingLogType.rx => ('RX', PingColors.rx),
PingLogType.disc => ('DISC', PingColors.discSuccess),
PingLogType.trace => ('TRC', PingColors.traceSuccess),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
Expand Down Expand Up @@ -833,7 +834,7 @@ class _AllPingsTabState extends State<_AllPingsTab> {
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Color(0xFF7B68EE),
color: PingColors.discSuccess,
),
),
],
Expand Down
Loading