fix(server): remove connection stats feature (#1063)

This commit is contained in:
lollipopkit🏳️‍⚧️
2026-03-07 02:06:45 +08:00
committed by GitHub
parent ac4607371a
commit 04b47a385a
8 changed files with 3 additions and 397 deletions

View File

@@ -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);
}

View File

@@ -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,
@@ -161,29 +151,7 @@ class ServerNotifier extends _$ServerNotifier {
TermSessionManager.setActive(sessionId, hasTerminal: false);
} 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);

View File

@@ -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

View File

@@ -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,
),
],
);
}
}

View File

@@ -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';

View File

@@ -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(

View File

@@ -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),

View File

@@ -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';