fix(server): remove connection stats feature (#1063)
This commit is contained in:
@@ -225,9 +225,6 @@ class ServersNotifier extends _$ServersNotifier {
|
||||
Stores.setting.serverOrder.put(newOrder);
|
||||
Stores.server.delete(id);
|
||||
|
||||
// Remove connection stats when server is deleted
|
||||
Stores.connectionStats.clearServerStats(id);
|
||||
|
||||
// Remove SSH session when server is deleted
|
||||
final sessionId = 'ssh_$id';
|
||||
TermSessionManager.remove(sessionId);
|
||||
@@ -246,7 +243,6 @@ class ServersNotifier extends _$ServersNotifier {
|
||||
|
||||
Stores.setting.serverOrder.put([]);
|
||||
Stores.server.clear();
|
||||
Stores.connectionStats.clearAll();
|
||||
bakSync.sync(milliDelay: 1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:server_box/data/helper/system_detector.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
import 'package:server_box/data/model/app/scripts/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/server_status_update_req.dart';
|
||||
@@ -141,15 +140,6 @@ class ServerNotifier extends _$ServerNotifier {
|
||||
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
|
||||
}
|
||||
|
||||
// Record successful connection
|
||||
Stores.connectionStats.recordConnection(ConnectionStat(
|
||||
serverId: spi.id,
|
||||
serverName: spi.name,
|
||||
timestamp: time1,
|
||||
result: ConnectionResult.success,
|
||||
durationMs: spentTime,
|
||||
));
|
||||
|
||||
final sessionId = 'ssh_${spi.id}';
|
||||
TermSessionManager.add(
|
||||
id: sessionId,
|
||||
@@ -162,28 +152,6 @@ class ServerNotifier extends _$ServerNotifier {
|
||||
} catch (e) {
|
||||
TryLimiter.inc(sid);
|
||||
|
||||
// Determine connection failure type
|
||||
ConnectionResult failureResult;
|
||||
if (e.toString().contains('timeout') || e.toString().contains('Timeout')) {
|
||||
failureResult = ConnectionResult.timeout;
|
||||
} else if (e.toString().contains('auth') || e.toString().contains('Authentication')) {
|
||||
failureResult = ConnectionResult.authFailed;
|
||||
} else if (e.toString().contains('network') || e.toString().contains('Network')) {
|
||||
failureResult = ConnectionResult.networkError;
|
||||
} else {
|
||||
failureResult = ConnectionResult.unknownError;
|
||||
}
|
||||
|
||||
// Record failed connection
|
||||
Stores.connectionStats.recordConnection(ConnectionStat(
|
||||
serverId: spi.id,
|
||||
serverName: spi.name,
|
||||
timestamp: DateTime.now(),
|
||||
result: failureResult,
|
||||
errorMessage: e.toString(),
|
||||
durationMs: 0,
|
||||
));
|
||||
|
||||
final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
|
||||
updateStatus(newStatus);
|
||||
updateConnection(ServerConn.failed);
|
||||
|
||||
@@ -17,6 +17,7 @@ abstract final class Stores {
|
||||
static PrivateKeyStore get key => getIt<PrivateKeyStore>();
|
||||
static SnippetStore get snippet => getIt<SnippetStore>();
|
||||
static HistoryStore get history => getIt<HistoryStore>();
|
||||
// Keep the legacy box registered so existing connection stats DB files remain intact.
|
||||
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
|
||||
|
||||
/// All stores that need backup
|
||||
|
||||
@@ -1,341 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/model/server/connection_stat.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
class ConnectionStatsPage extends StatefulWidget {
|
||||
const ConnectionStatsPage({super.key});
|
||||
|
||||
static const route = AppRouteNoArg(page: ConnectionStatsPage.new, path: '/server/conn_stats');
|
||||
|
||||
@override
|
||||
State<ConnectionStatsPage> createState() => _ConnectionStatsPageState();
|
||||
}
|
||||
|
||||
class _ConnectionStatsPageState extends State<ConnectionStatsPage> {
|
||||
List<ServerConnectionStats> _serverStats = [];
|
||||
bool _isLoading = true;
|
||||
bool _isCompacting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
void _loadStats() {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final stats = Stores.connectionStats.getAllServerStats();
|
||||
setState(() {
|
||||
_serverStats = stats;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(l10n.connectionStats),
|
||||
actions: [
|
||||
IconButton(onPressed: _loadStats, icon: const Icon(Icons.refresh), tooltip: libL10n.refresh),
|
||||
IconButton(
|
||||
onPressed: _showClearAllDialog,
|
||||
icon: const Icon(Icons.clear_all, color: Colors.red),
|
||||
tooltip: libL10n.clear,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _isCompacting ? null : _showCompactDialog,
|
||||
icon: _isCompacting
|
||||
? SizedLoading.small
|
||||
: const Icon(Icons.compress),
|
||||
tooltip: l10n.compactDatabase,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildBody,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildBody {
|
||||
if (_isLoading) {
|
||||
return const Center(child: SizedLoading.large);
|
||||
}
|
||||
if (_serverStats.isEmpty) {
|
||||
return Center(child: Text(l10n.noConnectionStatsData));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _serverStats.length,
|
||||
itemBuilder: (context, index) {
|
||||
final stats = _serverStats[index];
|
||||
return _buildServerStatsCard(stats);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServerStatsCard(ServerConnectionStats stats) {
|
||||
final successRate = stats.totalAttempts == 0 ? 'N/A' : '${(stats.successRate * 100).toStringAsFixed(1)}%';
|
||||
final lastSuccessTime = stats.lastSuccessTime;
|
||||
final lastFailureTime = stats.lastFailureTime;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
stats.serverName,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${libL10n.success}: $successRate',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: stats.successRate >= 0.8
|
||||
? Colors.green
|
||||
: stats.successRate >= 0.5
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildStatItem(libL10n.totalAttempts, stats.totalAttempts.toString(), Icons.all_inclusive),
|
||||
_buildStatItem(
|
||||
libL10n.success,
|
||||
stats.successCount.toString(),
|
||||
Icons.check_circle,
|
||||
Colors.green,
|
||||
),
|
||||
_buildStatItem(libL10n.fail, stats.failureCount.toString(), Icons.error, Colors.red),
|
||||
],
|
||||
),
|
||||
if (lastSuccessTime != null || lastFailureTime != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
if (lastSuccessTime != null)
|
||||
_buildTimeItem(l10n.lastSuccess, lastSuccessTime, Icons.check_circle, Colors.green),
|
||||
if (lastFailureTime != null)
|
||||
_buildTimeItem(l10n.lastFailure, lastFailureTime, Icons.error, Colors.red),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(l10n.recentConnections, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
TextButton(onPressed: () => _showServerDetailsDialog(stats), child: Text(l10n.viewDetails)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...stats.recentConnections.take(3).map(_buildConnectionItem),
|
||||
],
|
||||
),
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value, IconData icon, [Color? color]) {
|
||||
return Column(
|
||||
children: [
|
||||
Icon(icon, size: 24, color: color ?? Colors.grey),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: color),
|
||||
),
|
||||
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeItem(String label, DateTime time, IconData icon, Color color) {
|
||||
final timeStr = time.simple();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
UIs.width7,
|
||||
Text('$label: ', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
|
||||
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionItem(ConnectionStat stat) {
|
||||
final timeStr = stat.timestamp.simple();
|
||||
final isSuccess = stat.result.isSuccess;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
size: 16,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
UIs.width7,
|
||||
Text(timeStr, style: const TextStyle(fontSize: 12)),
|
||||
UIs.width7,
|
||||
Expanded(
|
||||
child: Text(
|
||||
isSuccess ? '${libL10n.success} (${stat.durationMs}ms)' : stat.result.displayName,
|
||||
style: TextStyle(fontSize: 12, color: isSuccess ? Colors.green : Colors.red),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCompactDialog() {
|
||||
final path = '${Paths.doc}${Pfs.seperator}connection_stats_enc.hive';
|
||||
final file = File(path);
|
||||
final oldSize = file.existsSync() ? file.lengthSync() : 0;
|
||||
final sizeStr = oldSize < 1000 ? '$oldSize B' : oldSize < 1000 * 1000 ? '${(oldSize / 1000).toStringAsFixed(1)} KB' : '${(oldSize / (1000 * 1000)).toStringAsFixed(1)} MB';
|
||||
|
||||
context.showRoundDialog(
|
||||
title: l10n.compactDatabase,
|
||||
child: Text(l10n.compactDatabaseContent(sizeStr)),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
context.pop();
|
||||
setState(() => _isCompacting = true);
|
||||
try {
|
||||
await Stores.connectionStats.compact();
|
||||
final newSize = file.existsSync() ? file.lengthSync() : 0;
|
||||
final newSizeStr = newSize < 1000 ? '$newSize B' : newSize < 1000 * 1000 ? '${(newSize / 1000).toStringAsFixed(1)} KB' : '${(newSize / (1000 * 1000)).toStringAsFixed(1)} MB';
|
||||
if (mounted) {
|
||||
setState(() => _isCompacting = false);
|
||||
context.showSnackBar('${libL10n.success}: $sizeStr -> $newSizeStr');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isCompacting = false);
|
||||
context.showSnackBar('${libL10n.error}: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(libL10n.confirm),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on _ConnectionStatsPageState {
|
||||
void _showServerDetailsDialog(ServerConnectionStats stats) {
|
||||
context.showRoundDialog(
|
||||
title: '${stats.serverName} - ${l10n.connectionDetails}',
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: MediaQuery.sizeOf(context).height * 0.7,
|
||||
child: ListView.separated(
|
||||
itemCount: stats.recentConnections.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final stat = stats.recentConnections[index];
|
||||
final timeStr = stat.timestamp.simple();
|
||||
final isSuccess = stat.result.isSuccess;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: isSuccess ? Colors.green : Colors.red,
|
||||
),
|
||||
title: Text(timeStr),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isSuccess
|
||||
? '${libL10n.success} (${stat.durationMs}ms)'
|
||||
: '${libL10n.fail}: ${stat.result.displayName}',
|
||||
style: TextStyle(color: isSuccess ? Colors.green : Colors.red),
|
||||
),
|
||||
if (!isSuccess && stat.errorMessage.isNotEmpty)
|
||||
Text(
|
||||
stat.errorMessage,
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.close)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_showClearServerStatsDialog(stats);
|
||||
},
|
||||
child: Text(l10n.clearThisServerStats, style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearAllDialog() {
|
||||
context.showRoundDialog(
|
||||
title: l10n.clearAllStatsTitle,
|
||||
child: Text(l10n.clearAllStatsContent),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||
CountDownBtn(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
Stores.connectionStats.clearAll();
|
||||
_loadStats();
|
||||
},
|
||||
text: libL10n.ok,
|
||||
afterColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearServerStatsDialog(ServerConnectionStats stats) {
|
||||
context.showRoundDialog(
|
||||
title: l10n.clearServerStatsTitle(stats.serverName),
|
||||
child: Text(l10n.clearServerStatsContent(stats.serverName)),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text(libL10n.cancel)),
|
||||
CountDownBtn(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
Stores.connectionStats.clearServerStats(stats.serverId);
|
||||
_loadStats();
|
||||
},
|
||||
text: libL10n.ok,
|
||||
afterColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import 'package:server_box/data/provider/server/all.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/server/connection_stats.dart';
|
||||
import 'package:server_box/view/page/server/detail/view.dart';
|
||||
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||
import 'package:server_box/view/page/setting/entry.dart';
|
||||
|
||||
@@ -42,10 +42,7 @@ final class _TopBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||
}
|
||||
final total = order.length;
|
||||
final connectionText = '$connected/$total ${context.libL10n.conn}';
|
||||
leading = InkWell(
|
||||
onTap: () => ConnectionStatsPage.route.go(context),
|
||||
child: Text(connectionText, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
);
|
||||
leading = Text(connectionText, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600));
|
||||
}
|
||||
|
||||
return Padding(
|
||||
|
||||
@@ -9,7 +9,6 @@ extension _Server on _AppSettingsPageState {
|
||||
_buildNetViewType(),
|
||||
_buildServerSeq(),
|
||||
_buildServerDetailCardSeq(),
|
||||
_buildConnectionStats(),
|
||||
_buildDeleteServers(),
|
||||
_buildCpuView(),
|
||||
_buildServerMore(),
|
||||
@@ -39,18 +38,6 @@ extension _Server on _AppSettingsPageState {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionStats() {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.analytics, size: _kIconSize),
|
||||
title: Text(l10n.connectionStats),
|
||||
subtitle: Text(l10n.connectionStatsDesc),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () {
|
||||
ConnectionStatsPage.route.go(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteServers() {
|
||||
return ListTile(
|
||||
title: Text(l10n.deleteServers),
|
||||
|
||||
@@ -18,7 +18,6 @@ import 'package:server_box/data/store/setting.dart';
|
||||
import 'package:server_box/generated/l10n/l10n.dart';
|
||||
import 'package:server_box/view/page/backup.dart';
|
||||
import 'package:server_box/view/page/private_key/list.dart';
|
||||
import 'package:server_box/view/page/server/connection_stats.dart';
|
||||
import 'package:server_box/view/page/setting/entries/home_tabs.dart';
|
||||
import 'package:server_box/view/page/setting/platform/ios.dart';
|
||||
import 'package:server_box/view/page/setting/platform/platform_pub.dart';
|
||||
|
||||
Reference in New Issue
Block a user