fix: Widen worker stats graph

This commit is contained in:
Janez T
2026-04-04 20:20:36 +02:00
parent 5320fa1a2b
commit 1baab06627
13 changed files with 355 additions and 1345 deletions

View File

@@ -6,11 +6,9 @@ import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../../models/contact.dart';
import '../../models/path_history.dart';
import '../../l10n/app_localizations.dart';
import '../../providers/app_provider.dart';
import '../../providers/connection_provider.dart';
import '../../services/path_history_service.dart';
import '../../services/relay_candidate_sorter.dart';
import '../../services/route_hash_preferences.dart';
@@ -72,7 +70,6 @@ class ContactRouteDialog extends StatefulWidget {
class _ContactRouteDialogState extends State<ContactRouteDialog> {
late final TextEditingController _controller;
late final TextEditingController _relaySearchController;
final PathHistoryService _pathHistoryService = PathHistoryService();
final RelayCandidateSorter _relayCandidateSorter =
const RelayCandidateSorter();
int _selectedHashSize = RouteHashPreferences.defaultHashSize;
@@ -80,7 +77,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
String? _errorText;
bool _showRoutingInfo = false;
List<Contact> _selectedMapHops = const [];
ContactPathHistory? _pathHistory;
@override
void initState() {
@@ -91,7 +87,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_relaySearchController = TextEditingController();
_controller.addListener(_reparse);
_loadHashSizePreference();
_loadPathHistory();
_reparse();
}
@@ -182,16 +177,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_reparse();
}
Future<void> _loadPathHistory() async {
await _pathHistoryService.initialize();
if (!mounted) return;
setState(() {
_pathHistory = _pathHistoryService.historyFor(
widget.contact.publicKeyHex,
);
});
}
String _tokenFor(Contact contact, int hashSize) {
final hex = contact.publicKeyHex.toUpperCase();
final length = hashSize * 2;
@@ -237,21 +222,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
});
}
void _applyHistoryRecord(PathRecord record) {
final canonicalText = _canonicalRouteFromBytes(
record.pathBytes,
hashSize: record.hashSize,
);
setState(() {
_controller.text = canonicalText;
_controller.selection = TextSelection.fromPosition(
TextPosition(offset: _controller.text.length),
);
_errorText = null;
});
_reparse();
}
LatLng? _resolveLastHopLocation() {
if (_selectedMapHops.isNotEmpty) {
return _selectedMapHops.last.displayLocation == null
@@ -302,86 +272,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
);
}
String _canonicalRouteFromBytes(
List<int> pathBytes, {
required int hashSize,
}) {
final hops = <String>[];
for (var i = 0; i < pathBytes.length; i += hashSize) {
final hop = pathBytes.sublist(i, i + hashSize);
hops.add(
hop
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase(),
);
}
return hops.join(',');
}
String _historySubtitle(PathRecord record) {
final attempts = record.successCount + record.failureCount;
final lastSeen = MaterialLocalizations.of(
context,
).formatShortDate(record.lastUsedAt);
final sourceLabel = switch (record.source) {
PathRecordSource.observed => 'Observed on mesh',
PathRecordSource.learned => 'Learned route',
};
final successRate = attempts == 0
? 'No send stats yet'
: '${(record.successRate * 100).round()}% success over $attempts send${attempts == 1 ? '' : 's'}';
final latency = record.lastRoundTripTimeMs > 0
? '${record.lastRoundTripTimeMs} ms'
: '';
return '$sourceLabel$successRate • Last used $lastSeen$latency';
}
Widget _buildHistoryRecordTile(PathRecord record, {String? title}) {
final canonicalText = _canonicalRouteFromBytes(
record.pathBytes,
hashSize: record.hashSize,
);
return Card(
margin: EdgeInsets.zero,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
leading: title == null ? null : const Icon(Icons.alt_route),
title: title == null
? Text(
canonicalText,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 6),
Text(
canonicalText,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
),
],
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(_historySubtitle(record)),
),
trailing: FilledButton.tonal(
onPressed: () => _applyHistoryRecord(record),
child: Text(AppLocalizations.of(context)!.use),
),
),
);
}
Widget _buildPreviewSection() {
final previewRoute = _effectiveRoute;
if (previewRoute == null) {
@@ -676,7 +566,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_showRoutingInfo = !_showRoutingInfo;
});
},
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
nearestRelayFallbackEnabled: appProvider.nearestRelayFallbackEnabled,
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
),
@@ -685,79 +574,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
);
}
Widget _buildHistoryTab() {
final records = List<PathRecord>.from(_pathHistory?.directPaths ?? const [])
..sort((a, b) => b.lastUsedAt.compareTo(a.lastUsedAt));
if (records.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
'No historical paths for this contact yet.',
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
);
}
PathRecord? observedRecord;
for (final record in records) {
if (record.source == PathRecordSource.observed) {
observedRecord = record;
break;
}
}
final remainingRecords = observedRecord == null
? records
: records
.where((record) => !identical(record, observedRecord))
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () async {
await _pathHistoryService.clearHistoryForContact(widget.contact);
if (!mounted) return;
setState(() {
_pathHistory = _pathHistoryService.historyFor(
widget.contact.publicKeyHex,
);
});
},
child: const Text('Clear history'),
),
),
const SizedBox(height: 8),
if (observedRecord != null) ...[
_buildHistoryRecordTile(observedRecord, title: AppLocalizations.of(context)!.observedMeshRoute),
const SizedBox(height: 16),
],
if (remainingRecords.isEmpty)
Text(
observedRecord == null
? 'No additional route history yet.'
: 'Observed routes you start using will continue to build history here.',
style: Theme.of(context).textTheme.bodyMedium,
)
else
ListView.separated(
itemCount: remainingRecords.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
return _buildHistoryRecordTile(remainingRecords[index]);
},
),
],
);
}
@override
Widget build(BuildContext context) {
final effectiveRoute = _effectiveRoute;
@@ -802,14 +618,13 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
];
return DefaultTabController(
length: 3,
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Set Path for ${widget.contact.displayName}'),
bottom: const TabBar(
tabs: [
Tab(text: 'Build'),
Tab(text: 'History'),
Tab(text: 'Info'),
],
),
@@ -827,12 +642,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
const SizedBox(height: 24),
],
),
ListView(
children: [
_buildHistoryTab(),
const SizedBox(height: 24),
],
),
_buildInfoTab(
appProvider: appProvider,
routeCandidates: routeCandidates,
@@ -912,14 +721,12 @@ class _RouteMarkerDot extends StatelessWidget {
class _AutomationRoutingInfo extends StatelessWidget {
final bool isExpanded;
final VoidCallback onToggle;
final bool autoRouteRotationEnabled;
final bool nearestRelayFallbackEnabled;
final bool clearPathOnMaxRetry;
const _AutomationRoutingInfo({
required this.isExpanded,
required this.onToggle,
required this.autoRouteRotationEnabled,
required this.nearestRelayFallbackEnabled,
required this.clearPathOnMaxRetry,
});
@@ -968,7 +775,7 @@ class _AutomationRoutingInfo extends StatelessWidget {
if (isExpanded) ...[
const SizedBox(height: 8),
Text(
'Room/contact sends keep one selected path for the whole send chain, retry up to 5 total attempts with 1s, 2s, 4s, and 8s backoff, then try one final nearest repeater if everything else fails.',
'Room/contact sends use the current direct path when one is known, switch to flood on the last normal retry, then try one final nearest repeater if everything else fails.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 8),
@@ -981,12 +788,6 @@ class _AutomationRoutingInfo extends StatelessWidget {
spacing: 8,
runSpacing: 8,
children: [
_InfoChip(
label: autoRouteRotationEnabled
? 'Auto route rotation on'
: 'Auto route rotation off',
icon: Icons.swap_horiz,
),
_InfoChip(
label: nearestRelayFallbackEnabled
? 'Nearest repeater fallback on'
@@ -1004,7 +805,7 @@ class _AutomationRoutingInfo extends StatelessWidget {
] else ...[
const SizedBox(height: 6),
Text(
'Shows retry, rotation, and final repeater fallback behavior.',
'Shows retry and final repeater fallback behavior.',
style: Theme.of(context).textTheme.bodySmall,
),
],

View File

@@ -850,12 +850,6 @@ class ContactTile extends StatelessWidget {
signedEncodedPathLen: parsedRoute.signedEncodedPathLen,
paddedPathBytes: parsedRoute.paddedPathBytes,
);
await pathHistoryService.clearHistoryForContact(
contact.copyWith(
outPathLen: parsedRoute.signedEncodedPathLen,
outPath: Uint8List.fromList(parsedRoute.paddedPathBytes),
),
);
await pathHistoryService.setManualRouteForContact(contact, parsedRoute);
if (context.mounted) {
final routeLabel = parsedRoute.hopCount == 0