feat(#550): show observers + observations; top-banner refresh feedback

Badge stays observers-primary; tooltip/long-press now read 'N observers . M
observations' so they reconcile with CoreScope's 'Observations (N)' feed (the
two are distinct metrics: 15 distinct observers vs 34 total sightings). Refresh
feedback moved from a bottom snackbar to an auto-dismissed top MaterialBanner,
out of the way of the composer. Part of #524.

Agent: QuietSnow (session 31eaba02)
pull/556/head
Strycher 2 months ago
parent 61b17a3aa4
commit 9e3500a9a9

@ -4789,17 +4789,23 @@ class MeshCoreConnector extends ChangeNotifier {
for (final secs in _coreScopePollSecs) { for (final secs in _coreScopePollSecs) {
await Future<void>.delayed(Duration(seconds: secs)); await Future<void>.delayed(Duration(seconds: secs));
if (!isConnected) return; if (!isConnected) return;
final count = await _coreScopeService.fetchObserverCount(pkt.hashHex); final counts = await _coreScopeService.fetchCounts(pkt.hashHex);
if (count == null) continue; if (counts == null) continue;
if (count > best) { if (counts.observers > best) {
best = count; best = counts.observers;
flat = 0; flat = 0;
_updateChannelMessageById( _updateChannelMessageById(
channelIndex, channelIndex,
messageId, messageId,
(m) => m.copyWith(coreScopeObserverCount: count), (m) => m.copyWith(
coreScopeObserverCount: counts.observers,
coreScopeObservationCount: counts.observations,
),
);
appLogger.info(
'observers=${counts.observers} observations=${counts.observations}',
tag: 'CoreScope',
); );
appLogger.info('observer count=$count', tag: 'CoreScope');
notifyListeners(); notifyListeners();
} else if (++flat >= _coreScopeStableChecks) { } else if (++flat >= _coreScopeStableChecks) {
return; // stabilised return; // stabilised
@ -4814,21 +4820,26 @@ class MeshCoreConnector extends ChangeNotifier {
} }
/// Tap-to-refresh: re-query CoreScope for a message's stored on-air hash and /// Tap-to-refresh: re-query CoreScope for a message's stored on-air hash and
/// bump the observer count if it grew (counts only ever climb). Returns the /// bump the counts if they grew (they only ever climb). Returns the fetched
/// fetched count (or null on failure / no stored hash) so the UI can confirm. /// counts (or null on failure / no stored hash) so the UI can confirm.
Future<int?> refreshCoreScopeObserverCount( Future<CoreScopeCounts?> refreshCoreScopeCounts(
int channelIndex, int channelIndex,
String messageId, String messageId,
String hashHex, String hashHex,
) async { ) async {
final count = await _coreScopeService.fetchObserverCount(hashHex); final counts = await _coreScopeService.fetchCounts(hashHex);
if (count == null) return null; if (counts == null) return null;
_updateChannelMessageById(channelIndex, messageId, (m) { _updateChannelMessageById(channelIndex, messageId, (m) {
final current = m.coreScopeObserverCount ?? 0; final current = m.coreScopeObserverCount ?? 0;
return count > current ? m.copyWith(coreScopeObserverCount: count) : m; return counts.observers >= current
? m.copyWith(
coreScopeObserverCount: counts.observers,
coreScopeObservationCount: counts.observations,
)
: m;
}); });
notifyListeners(); notifyListeners();
return count; return counts;
} }
void _updateChannelMessageById( void _updateChannelMessageById(

@ -886,6 +886,17 @@
}, },
"channel_coreScopeRefresh": "Refresh CoreScope observers", "channel_coreScopeRefresh": "Refresh CoreScope observers",
"channel_coreScopeRefreshFailed": "CoreScope unreachable", "channel_coreScopeRefreshFailed": "CoreScope unreachable",
"channel_coreScopeCounts": "{observers, plural, =1{1 observer} other{{observers} observers}} · {observations} observations",
"@channel_coreScopeCounts": {
"placeholders": {
"observers": {
"type": "int"
},
"observations": {
"type": "int"
}
}
},
"chat_successes": "successes", "chat_successes": "successes",
"chat_score": "Score", "chat_score": "Score",
"chat_removePath": "Remove path", "chat_removePath": "Remove path",

@ -3100,6 +3100,12 @@ abstract class AppLocalizations {
/// **'CoreScope unreachable'** /// **'CoreScope unreachable'**
String get channel_coreScopeRefreshFailed; String get channel_coreScopeRefreshFailed;
/// No description provided for @channel_coreScopeCounts.
///
/// In en, this message translates to:
/// **'{observers, plural, =1{1 observer} other{{observers} observers}} · {observations} observations'**
String channel_coreScopeCounts(int observers, int observations);
/// No description provided for @chat_successes. /// No description provided for @chat_successes.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

@ -1719,6 +1719,17 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Успехи'; String get chat_successes => 'Успехи';

@ -1717,6 +1717,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Erfolgreich'; String get chat_successes => 'Erfolgreich';

@ -1687,6 +1687,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'successes'; String get chat_successes => 'successes';

@ -1714,6 +1714,17 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Éxitos'; String get chat_successes => 'Éxitos';

@ -1721,6 +1721,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Succès'; String get chat_successes => 'Succès';

@ -1726,6 +1726,17 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'sikerek'; String get chat_successes => 'sikerek';

@ -1716,6 +1716,17 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'successi'; String get chat_successes => 'successi';

@ -1651,6 +1651,17 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => '成功事例'; String get chat_successes => '成功事例';

@ -1647,6 +1647,17 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => '성공 사례'; String get chat_successes => '성공 사례';

@ -1704,6 +1704,17 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Succesvol'; String get chat_successes => 'Succesvol';

@ -1729,6 +1729,17 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Sukcesy'; String get chat_successes => 'Sukcesy';

@ -1713,6 +1713,17 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Sucessos'; String get chat_successes => 'Sucessos';

@ -1717,6 +1717,17 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'успешно'; String get chat_successes => 'успешно';

@ -1706,6 +1706,17 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Úspechy'; String get chat_successes => 'Úspechy';

@ -1702,6 +1702,17 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Uspešni'; String get chat_successes => 'Uspešni';

@ -1697,6 +1697,17 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'framgångar'; String get chat_successes => 'framgångar';

@ -1712,6 +1712,17 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => 'Успішно'; String get chat_successes => 'Успішно';

@ -1626,6 +1626,17 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get channel_coreScopeRefreshFailed => 'CoreScope unreachable'; String get channel_coreScopeRefreshFailed => 'CoreScope unreachable';
@override
String channel_coreScopeCounts(int observers, int observations) {
String _temp0 = intl.Intl.pluralLogic(
observers,
locale: localeName,
other: '$observers observers',
one: '1 observer',
);
return '$_temp0 · $observations observations';
}
@override @override
String get chat_successes => '成功'; String get chat_successes => '成功';

@ -56,6 +56,10 @@ class ChannelMessage {
/// Unique observers CoreScope reports for this message's on-air packet /// Unique observers CoreScope reports for this message's on-air packet
/// (#524). Null until queried / when the feature is off. Transient. /// (#524). Null until queried / when the feature is off. Transient.
final int? coreScopeObserverCount; final int? coreScopeObserverCount;
/// Total observations (sightings) CoreScope reports for the same packet, the
/// "Observations (N)" number in its UI. Transient. See [coreScopeObserverCount].
final int? coreScopeObservationCount;
final String? replyToMessageId; final String? replyToMessageId;
final String? replyToSenderName; final String? replyToSenderName;
final String? replyToText; final String? replyToText;
@ -96,6 +100,7 @@ class ChannelMessage {
this.packetHash, this.packetHash,
this.onAirHash, this.onAirHash,
this.coreScopeObserverCount, this.coreScopeObserverCount,
this.coreScopeObservationCount,
this.replyToMessageId, this.replyToMessageId,
this.replyToSenderName, this.replyToSenderName,
this.replyToText, this.replyToText,
@ -144,6 +149,7 @@ class ChannelMessage {
String? packetHash, String? packetHash,
String? onAirHash, String? onAirHash,
int? coreScopeObserverCount, int? coreScopeObserverCount,
int? coreScopeObservationCount,
String? replyToMessageId, String? replyToMessageId,
String? replyToSenderName, String? replyToSenderName,
String? replyToText, String? replyToText,
@ -187,6 +193,8 @@ class ChannelMessage {
onAirHash: onAirHash ?? this.onAirHash, onAirHash: onAirHash ?? this.onAirHash,
coreScopeObserverCount: coreScopeObserverCount:
coreScopeObserverCount ?? this.coreScopeObserverCount, coreScopeObserverCount ?? this.coreScopeObserverCount,
coreScopeObservationCount:
coreScopeObservationCount ?? this.coreScopeObservationCount,
replyToMessageId: replyToMessageId ?? this.replyToMessageId, replyToMessageId: replyToMessageId ?? this.replyToMessageId,
replyToSenderName: replyToSenderName ?? this.replyToSenderName, replyToSenderName: replyToSenderName ?? this.replyToSenderName,
replyToText: replyToText ?? this.replyToText, replyToText: replyToText ?? this.replyToText,

@ -1523,9 +1523,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (isOutgoing && (message.coreScopeObserverCount ?? 0) > 0) ...[ if (isOutgoing && (message.coreScopeObserverCount ?? 0) > 0) ...[
dot, dot,
Tooltip( Tooltip(
message: context.l10n.channel_coreScopeTooltip( message: _coreScopeLabel(message),
message.coreScopeObserverCount!,
),
child: InkWell( child: InkWell(
onTap: message.onAirHash == null onTap: message.onAirHash == null
? null ? null
@ -1642,25 +1640,52 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
String _coreScopeLabel(ChannelMessage message) {
final observers = message.coreScopeObserverCount ?? 0;
final observations = message.coreScopeObservationCount;
return observations == null
? context.l10n.channel_coreScopeTooltip(observers)
: context.l10n.channel_coreScopeCounts(observers, observations);
}
Future<void> _refreshCoreScope(ChannelMessage message) async { Future<void> _refreshCoreScope(ChannelMessage message) async {
final hash = message.onAirHash; final hash = message.onAirHash;
if (hash == null) return; if (hash == null) return;
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
final l10n = context.l10n; final l10n = context.l10n;
final count = await connector.refreshCoreScopeObserverCount( final counts = await connector.refreshCoreScopeCounts(
message.channelIndex ?? _currentChannel.index, message.channelIndex ?? _currentChannel.index,
message.messageId, message.messageId,
hash, hash,
); );
if (!mounted) return; if (!mounted) return;
showDismissibleSnackBar( _showCoreScopeBanner(
context, counts == null
content: Text(
count == null
? l10n.channel_coreScopeRefreshFailed ? l10n.channel_coreScopeRefreshFailed
: l10n.channel_coreScopeTooltip(count), : l10n.channel_coreScopeCounts(counts.observers, counts.observations),
);
}
/// Top banner (out of the way of the composer), auto-dismissed after a few
/// seconds. Used for the CoreScope refresh result (#524).
void _showCoreScopeBanner(String message) {
final messenger = ScaffoldMessenger.of(context);
messenger.clearMaterialBanners();
messenger.showMaterialBanner(
MaterialBanner(
content: Text(message),
leading: const Icon(Icons.cloud_outlined),
actions: [
TextButton(
onPressed: messenger.hideCurrentMaterialBanner,
child: Text(context.l10n.common_ok),
),
],
), ),
); );
Future.delayed(const Duration(seconds: 4), () {
if (mounted) messenger.hideCurrentMaterialBanner();
});
} }
void _retryChannelMessage(ChannelMessage message) { void _retryChannelMessage(ChannelMessage message) {
@ -1736,11 +1761,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
leading: const Icon(Icons.cloud_outlined), leading: const Icon(Icons.cloud_outlined),
title: Text(context.l10n.channel_coreScopeRefresh), title: Text(context.l10n.channel_coreScopeRefresh),
subtitle: message.coreScopeObserverCount != null subtitle: message.coreScopeObserverCount != null
? Text( ? Text(_coreScopeLabel(message))
context.l10n.channel_coreScopeTooltip(
message.coreScopeObserverCount!,
),
)
: null, : null,
onTap: () { onTap: () {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);

@ -7,6 +7,16 @@ import '../utils/app_logger.dart';
/// Default CoreScope instance (OKIMesh). Owner-run; its read API is public. /// Default CoreScope instance (OKIMesh). Owner-run; its read API is public.
const String kDefaultCoreScopeHost = 'map.okimesh.org'; const String kDefaultCoreScopeHost = 'map.okimesh.org';
/// The two counts CoreScope reports for a packet. [observers] is the number of
/// distinct observers (the meaningful reach); [observations] is the total
/// sightings, which counts one observer hearing the packet via several paths
/// more than once (this is the "Observations (N)" number in CoreScope's UI).
class CoreScopeCounts {
const CoreScopeCounts({required this.observers, required this.observations});
final int observers;
final int observations;
}
/// Queries a CoreScope instance for how many observers reported a given packet, /// Queries a CoreScope instance for how many observers reported a given packet,
/// keyed by the firmware/mesh packet hash (#524). Read-only and best-effort: /// keyed by the firmware/mesh packet hash (#524). Read-only and best-effort:
/// any failure (offline, timeout, non-200, bad body, unknown packet) returns /// any failure (offline, timeout, non-200, bad body, unknown packet) returns
@ -24,10 +34,10 @@ class CoreScopeService {
final bool useTls; final bool useTls;
final Duration timeout; final Duration timeout;
/// Unique observers that reported the packet with [packetHash] /// Distinct observers and total observations for [packetHash] (16 lowercase
/// (16 lowercase hex chars). Returns null on any error, or when CoreScope /// hex chars). Returns null on any error, or when CoreScope has no record of
/// has no record of the hash yet. /// the hash yet.
Future<int?> fetchObserverCount(String packetHash) async { Future<CoreScopeCounts?> fetchCounts(String packetHash) async {
if (packetHash.isEmpty) return null; if (packetHash.isEmpty) return null;
final uri = Uri( final uri = Uri(
scheme: useTls ? 'https' : 'http', scheme: useTls ? 'https' : 'http',
@ -58,13 +68,21 @@ class CoreScopeService {
} }
final first = packets.first; final first = packets.first;
if (first is! Map) return null; if (first is! Map) return null;
final count = first['observer_count']; final observers = first['observer_count'];
final result = count is num ? count.toInt() : null; if (observers is! num) return null;
final observations = first['observation_count'];
final counts = CoreScopeCounts(
observers: observers.toInt(),
observations: observations is num
? observations.toInt()
: observers.toInt(),
);
appLogger.info( appLogger.info(
'observer_count=$result for $packetHash', 'observers=${counts.observers} observations=${counts.observations} '
'for $packetHash',
tag: 'CoreScope', tag: 'CoreScope',
); );
return result; return counts;
} catch (e) { } catch (e) {
appLogger.warn('Query failed for $packetHash: $e', tag: 'CoreScope'); appLogger.warn('Query failed for $packetHash: $e', tag: 'CoreScope');
return null; return null;

@ -4,27 +4,43 @@ import 'package:http/testing.dart';
import 'package:meshcore_open/services/corescope_service.dart'; import 'package:meshcore_open/services/corescope_service.dart';
void main() { void main() {
group('CoreScopeService.fetchObserverCount', () { group('CoreScopeService.fetchCounts', () {
test('parses observer_count from a grouped-by-hash response', () async { test(
'parses observer + observation counts from a grouped response',
() async {
late Uri captured; late Uri captured;
final service = CoreScopeService( final service = CoreScopeService(
host: 'map.okimesh.org', host: 'map.okimesh.org',
client: MockClient((req) async { client: MockClient((req) async {
captured = req.url; captured = req.url;
return http.Response( return http.Response(
'{"packets":[{"hash":"abc123","observer_count":7}],"total":1}', '{"packets":[{"hash":"abc123","observer_count":15,'
'"observation_count":34}],"total":1}',
200, 200,
); );
}), }),
); );
expect(await service.fetchObserverCount('abc123'), 7); final counts = await service.fetchCounts('abc123');
// Correct endpoint + query built from the hash. expect(counts, isNotNull);
expect(captured.scheme, 'https'); expect(counts!.observers, 15);
expect(counts.observations, 34);
expect(captured.host, 'map.okimesh.org'); expect(captured.host, 'map.okimesh.org');
expect(captured.path, '/api/packets'); expect(captured.path, '/api/packets');
expect(captured.queryParameters['hash'], 'abc123'); expect(captured.queryParameters['hash'], 'abc123');
expect(captured.queryParameters['groupByHash'], 'true'); expect(captured.queryParameters['groupByHash'], 'true');
},
);
test('falls back to observers when observation_count is absent', () async {
final service = CoreScopeService(
client: MockClient(
(_) async => http.Response('{"packets":[{"observer_count":9}]}', 200),
),
);
final counts = await service.fetchCounts('abc123');
expect(counts!.observers, 9);
expect(counts.observations, 9);
}); });
test('returns null when CoreScope has no record (empty packets)', () async { test('returns null when CoreScope has no record (empty packets)', () async {
@ -33,28 +49,28 @@ void main() {
(_) async => http.Response('{"packets":[],"total":0}', 200), (_) async => http.Response('{"packets":[],"total":0}', 200),
), ),
); );
expect(await service.fetchObserverCount('deadbeef'), isNull); expect(await service.fetchCounts('deadbeef'), isNull);
}); });
test('returns null on non-200', () async { test('returns null on non-200', () async {
final service = CoreScopeService( final service = CoreScopeService(
client: MockClient((_) async => http.Response('nope', 503)), client: MockClient((_) async => http.Response('nope', 503)),
); );
expect(await service.fetchObserverCount('abc123'), isNull); expect(await service.fetchCounts('abc123'), isNull);
}); });
test('returns null on malformed body', () async { test('returns null on malformed body', () async {
final service = CoreScopeService( final service = CoreScopeService(
client: MockClient((_) async => http.Response('not json', 200)), client: MockClient((_) async => http.Response('not json', 200)),
); );
expect(await service.fetchObserverCount('abc123'), isNull); expect(await service.fetchCounts('abc123'), isNull);
}); });
test('returns null (never throws) on transport failure', () async { test('returns null (never throws) on transport failure', () async {
final service = CoreScopeService( final service = CoreScopeService(
client: MockClient((_) async => throw Exception('offline')), client: MockClient((_) async => throw Exception('offline')),
); );
expect(await service.fetchObserverCount('abc123'), isNull); expect(await service.fetchCounts('abc123'), isNull);
}); });
test( test(
@ -67,7 +83,7 @@ void main() {
return http.Response('{}', 200); return http.Response('{}', 200);
}), }),
); );
expect(await service.fetchObserverCount(''), isNull); expect(await service.fetchCounts(''), isNull);
expect(called, isFalse); expect(called, isFalse);
}, },
); );

@ -54,6 +54,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -156,6 +157,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -258,6 +260,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -360,6 +363,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -462,6 +466,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -564,6 +569,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -666,6 +672,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -768,6 +775,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -870,6 +878,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -972,6 +981,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1074,6 +1084,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1176,6 +1187,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1278,6 +1290,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1380,6 +1393,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1482,6 +1496,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1584,6 +1599,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",
@ -1686,6 +1702,7 @@
"channel_coreScopeTooltip", "channel_coreScopeTooltip",
"channel_coreScopeRefresh", "channel_coreScopeRefresh",
"channel_coreScopeRefreshFailed", "channel_coreScopeRefreshFailed",
"channel_coreScopeCounts",
"map_alwaysShowNames", "map_alwaysShowNames",
"path_suggestedRoute", "path_suggestedRoute",
"path_searchRepeaters", "path_searchRepeaters",

Loading…
Cancel
Save

Powered by TurnKey Linux.