feat: server conn statistics (#888)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-09-02 19:41:56 +08:00
committed by GitHub
parent 929061213f
commit 2466341999
39 changed files with 2534 additions and 19 deletions

View File

@@ -0,0 +1,79 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_ce/hive.dart';
part 'connection_stat.freezed.dart';
part 'connection_stat.g.dart';
@freezed
@HiveType(typeId: 100)
abstract class ConnectionStat with _$ConnectionStat {
const factory ConnectionStat({
@HiveField(0) required String serverId,
@HiveField(1) required String serverName,
@HiveField(2) required DateTime timestamp,
@HiveField(3) required ConnectionResult result,
@HiveField(4) @Default('') String errorMessage,
@HiveField(5) required int durationMs,
}) = _ConnectionStat;
factory ConnectionStat.fromJson(Map<String, dynamic> json) =>
_$ConnectionStatFromJson(json);
}
@freezed
@HiveType(typeId: 101)
abstract class ServerConnectionStats with _$ServerConnectionStats {
const factory ServerConnectionStats({
@HiveField(0) required String serverId,
@HiveField(1) required String serverName,
@HiveField(2) required int totalAttempts,
@HiveField(3) required int successCount,
@HiveField(4) required int failureCount,
@HiveField(5) @Default(null) DateTime? lastSuccessTime,
@HiveField(6) @Default(null) DateTime? lastFailureTime,
@HiveField(7) @Default([]) List<ConnectionStat> recentConnections,
@HiveField(8) required double successRate,
}) = _ServerConnectionStats;
factory ServerConnectionStats.fromJson(Map<String, dynamic> json) =>
_$ServerConnectionStatsFromJson(json);
}
@HiveType(typeId: 102)
enum ConnectionResult {
@HiveField(0)
@JsonValue('success')
success,
@HiveField(1)
@JsonValue('timeout')
timeout,
@HiveField(2)
@JsonValue('auth_failed')
authFailed,
@HiveField(3)
@JsonValue('network_error')
networkError,
@HiveField(4)
@JsonValue('unknown_error')
unknownError,
}
extension ConnectionResultExtension on ConnectionResult {
String get displayName {
switch (this) {
case ConnectionResult.success:
return libL10n.success;
case ConnectionResult.timeout:
return '${libL10n.error}(timeout)';
case ConnectionResult.authFailed:
return '${libL10n.error}(auth)';
case ConnectionResult.networkError:
return '${libL10n.error}(${libL10n.network})';
case ConnectionResult.unknownError:
return '${libL10n.error}(${libL10n.unknown})';
}
}
bool get isSuccess => this == ConnectionResult.success;
}

View File

@@ -0,0 +1,585 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'connection_stat.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ConnectionStat {
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) DateTime get timestamp;@HiveField(3) ConnectionResult get result;@HiveField(4) String get errorMessage;@HiveField(5) int get durationMs;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ConnectionStatCopyWith<ConnectionStat> get copyWith => _$ConnectionStatCopyWithImpl<ConnectionStat>(this as ConnectionStat, _$identity);
/// Serializes this ConnectionStat to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
@override
String toString() {
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
}
}
/// @nodoc
abstract mixin class $ConnectionStatCopyWith<$Res> {
factory $ConnectionStatCopyWith(ConnectionStat value, $Res Function(ConnectionStat) _then) = _$ConnectionStatCopyWithImpl;
@useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
});
}
/// @nodoc
class _$ConnectionStatCopyWithImpl<$Res>
implements $ConnectionStatCopyWith<$Res> {
_$ConnectionStatCopyWithImpl(this._self, this._then);
final ConnectionStat _self;
final $Res Function(ConnectionStat) _then;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
return _then(_self.copyWith(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [ConnectionStat].
extension ConnectionStatPatterns on ConnectionStat {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ConnectionStat value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ConnectionStat value) $default,){
final _that = this;
switch (_that) {
case _ConnectionStat():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ConnectionStat value)? $default,){
final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs) $default,) {final _that = this;
switch (_that) {
case _ConnectionStat():
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) DateTime timestamp, @HiveField(3) ConnectionResult result, @HiveField(4) String errorMessage, @HiveField(5) int durationMs)? $default,) {final _that = this;
switch (_that) {
case _ConnectionStat() when $default != null:
return $default(_that.serverId,_that.serverName,_that.timestamp,_that.result,_that.errorMessage,_that.durationMs);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ConnectionStat implements ConnectionStat {
const _ConnectionStat({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.timestamp, @HiveField(3) required this.result, @HiveField(4) this.errorMessage = '', @HiveField(5) required this.durationMs});
factory _ConnectionStat.fromJson(Map<String, dynamic> json) => _$ConnectionStatFromJson(json);
@override@HiveField(0) final String serverId;
@override@HiveField(1) final String serverName;
@override@HiveField(2) final DateTime timestamp;
@override@HiveField(3) final ConnectionResult result;
@override@JsonKey()@HiveField(4) final String errorMessage;
@override@HiveField(5) final int durationMs;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ConnectionStatCopyWith<_ConnectionStat> get copyWith => __$ConnectionStatCopyWithImpl<_ConnectionStat>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ConnectionStatToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ConnectionStat&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.result, result) || other.result == result)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.durationMs, durationMs) || other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,timestamp,result,errorMessage,durationMs);
@override
String toString() {
return 'ConnectionStat(serverId: $serverId, serverName: $serverName, timestamp: $timestamp, result: $result, errorMessage: $errorMessage, durationMs: $durationMs)';
}
}
/// @nodoc
abstract mixin class _$ConnectionStatCopyWith<$Res> implements $ConnectionStatCopyWith<$Res> {
factory _$ConnectionStatCopyWith(_ConnectionStat value, $Res Function(_ConnectionStat) _then) = __$ConnectionStatCopyWithImpl;
@override @useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) DateTime timestamp,@HiveField(3) ConnectionResult result,@HiveField(4) String errorMessage,@HiveField(5) int durationMs
});
}
/// @nodoc
class __$ConnectionStatCopyWithImpl<$Res>
implements _$ConnectionStatCopyWith<$Res> {
__$ConnectionStatCopyWithImpl(this._self, this._then);
final _ConnectionStat _self;
final $Res Function(_ConnectionStat) _then;
/// Create a copy of ConnectionStat
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? timestamp = null,Object? result = null,Object? errorMessage = null,Object? durationMs = null,}) {
return _then(_ConnectionStat(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,result: null == result ? _self.result : result // ignore: cast_nullable_to_non_nullable
as ConnectionResult,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,durationMs: null == durationMs ? _self.durationMs : durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
mixin _$ServerConnectionStats {
@HiveField(0) String get serverId;@HiveField(1) String get serverName;@HiveField(2) int get totalAttempts;@HiveField(3) int get successCount;@HiveField(4) int get failureCount;@HiveField(5) DateTime? get lastSuccessTime;@HiveField(6) DateTime? get lastFailureTime;@HiveField(7) List<ConnectionStat> get recentConnections;@HiveField(8) double get successRate;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServerConnectionStatsCopyWith<ServerConnectionStats> get copyWith => _$ServerConnectionStatsCopyWithImpl<ServerConnectionStats>(this as ServerConnectionStats, _$identity);
/// Serializes this ServerConnectionStats to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other.recentConnections, recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(recentConnections),successRate);
@override
String toString() {
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
}
}
/// @nodoc
abstract mixin class $ServerConnectionStatsCopyWith<$Res> {
factory $ServerConnectionStatsCopyWith(ServerConnectionStats value, $Res Function(ServerConnectionStats) _then) = _$ServerConnectionStatsCopyWithImpl;
@useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
});
}
/// @nodoc
class _$ServerConnectionStatsCopyWithImpl<$Res>
implements $ServerConnectionStatsCopyWith<$Res> {
_$ServerConnectionStatsCopyWithImpl(this._self, this._then);
final ServerConnectionStats _self;
final $Res Function(ServerConnectionStats) _then;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
return _then(_self.copyWith(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
as DateTime?,recentConnections: null == recentConnections ? _self.recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// Adds pattern-matching-related methods to [ServerConnectionStats].
extension ServerConnectionStatsPatterns on ServerConnectionStats {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServerConnectionStats value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServerConnectionStats value) $default,){
final _that = this;
switch (_that) {
case _ServerConnectionStats():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServerConnectionStats value)? $default,){
final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate) $default,) {final _that = this;
switch (_that) {
case _ServerConnectionStats():
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@HiveField(0) String serverId, @HiveField(1) String serverName, @HiveField(2) int totalAttempts, @HiveField(3) int successCount, @HiveField(4) int failureCount, @HiveField(5) DateTime? lastSuccessTime, @HiveField(6) DateTime? lastFailureTime, @HiveField(7) List<ConnectionStat> recentConnections, @HiveField(8) double successRate)? $default,) {final _that = this;
switch (_that) {
case _ServerConnectionStats() when $default != null:
return $default(_that.serverId,_that.serverName,_that.totalAttempts,_that.successCount,_that.failureCount,_that.lastSuccessTime,_that.lastFailureTime,_that.recentConnections,_that.successRate);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ServerConnectionStats implements ServerConnectionStats {
const _ServerConnectionStats({@HiveField(0) required this.serverId, @HiveField(1) required this.serverName, @HiveField(2) required this.totalAttempts, @HiveField(3) required this.successCount, @HiveField(4) required this.failureCount, @HiveField(5) this.lastSuccessTime = null, @HiveField(6) this.lastFailureTime = null, @HiveField(7) final List<ConnectionStat> recentConnections = const [], @HiveField(8) required this.successRate}): _recentConnections = recentConnections;
factory _ServerConnectionStats.fromJson(Map<String, dynamic> json) => _$ServerConnectionStatsFromJson(json);
@override@HiveField(0) final String serverId;
@override@HiveField(1) final String serverName;
@override@HiveField(2) final int totalAttempts;
@override@HiveField(3) final int successCount;
@override@HiveField(4) final int failureCount;
@override@JsonKey()@HiveField(5) final DateTime? lastSuccessTime;
@override@JsonKey()@HiveField(6) final DateTime? lastFailureTime;
final List<ConnectionStat> _recentConnections;
@override@JsonKey()@HiveField(7) List<ConnectionStat> get recentConnections {
if (_recentConnections is EqualUnmodifiableListView) return _recentConnections;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_recentConnections);
}
@override@HiveField(8) final double successRate;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServerConnectionStatsCopyWith<_ServerConnectionStats> get copyWith => __$ServerConnectionStatsCopyWithImpl<_ServerConnectionStats>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ServerConnectionStatsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerConnectionStats&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.serverName, serverName) || other.serverName == serverName)&&(identical(other.totalAttempts, totalAttempts) || other.totalAttempts == totalAttempts)&&(identical(other.successCount, successCount) || other.successCount == successCount)&&(identical(other.failureCount, failureCount) || other.failureCount == failureCount)&&(identical(other.lastSuccessTime, lastSuccessTime) || other.lastSuccessTime == lastSuccessTime)&&(identical(other.lastFailureTime, lastFailureTime) || other.lastFailureTime == lastFailureTime)&&const DeepCollectionEquality().equals(other._recentConnections, _recentConnections)&&(identical(other.successRate, successRate) || other.successRate == successRate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,serverId,serverName,totalAttempts,successCount,failureCount,lastSuccessTime,lastFailureTime,const DeepCollectionEquality().hash(_recentConnections),successRate);
@override
String toString() {
return 'ServerConnectionStats(serverId: $serverId, serverName: $serverName, totalAttempts: $totalAttempts, successCount: $successCount, failureCount: $failureCount, lastSuccessTime: $lastSuccessTime, lastFailureTime: $lastFailureTime, recentConnections: $recentConnections, successRate: $successRate)';
}
}
/// @nodoc
abstract mixin class _$ServerConnectionStatsCopyWith<$Res> implements $ServerConnectionStatsCopyWith<$Res> {
factory _$ServerConnectionStatsCopyWith(_ServerConnectionStats value, $Res Function(_ServerConnectionStats) _then) = __$ServerConnectionStatsCopyWithImpl;
@override @useResult
$Res call({
@HiveField(0) String serverId,@HiveField(1) String serverName,@HiveField(2) int totalAttempts,@HiveField(3) int successCount,@HiveField(4) int failureCount,@HiveField(5) DateTime? lastSuccessTime,@HiveField(6) DateTime? lastFailureTime,@HiveField(7) List<ConnectionStat> recentConnections,@HiveField(8) double successRate
});
}
/// @nodoc
class __$ServerConnectionStatsCopyWithImpl<$Res>
implements _$ServerConnectionStatsCopyWith<$Res> {
__$ServerConnectionStatsCopyWithImpl(this._self, this._then);
final _ServerConnectionStats _self;
final $Res Function(_ServerConnectionStats) _then;
/// Create a copy of ServerConnectionStats
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? serverName = null,Object? totalAttempts = null,Object? successCount = null,Object? failureCount = null,Object? lastSuccessTime = freezed,Object? lastFailureTime = freezed,Object? recentConnections = null,Object? successRate = null,}) {
return _then(_ServerConnectionStats(
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
as String,serverName: null == serverName ? _self.serverName : serverName // ignore: cast_nullable_to_non_nullable
as String,totalAttempts: null == totalAttempts ? _self.totalAttempts : totalAttempts // ignore: cast_nullable_to_non_nullable
as int,successCount: null == successCount ? _self.successCount : successCount // ignore: cast_nullable_to_non_nullable
as int,failureCount: null == failureCount ? _self.failureCount : failureCount // ignore: cast_nullable_to_non_nullable
as int,lastSuccessTime: freezed == lastSuccessTime ? _self.lastSuccessTime : lastSuccessTime // ignore: cast_nullable_to_non_nullable
as DateTime?,lastFailureTime: freezed == lastFailureTime ? _self.lastFailureTime : lastFailureTime // ignore: cast_nullable_to_non_nullable
as DateTime?,recentConnections: null == recentConnections ? _self._recentConnections : recentConnections // ignore: cast_nullable_to_non_nullable
as List<ConnectionStat>,successRate: null == successRate ? _self.successRate : successRate // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
// dart format on

View File

@@ -0,0 +1,233 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connection_stat.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ConnectionStatAdapter extends TypeAdapter<ConnectionStat> {
@override
final typeId = 100;
@override
ConnectionStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ConnectionStat(
serverId: fields[0] as String,
serverName: fields[1] as String,
timestamp: fields[2] as DateTime,
result: fields[3] as ConnectionResult,
errorMessage: fields[4] == null ? '' : fields[4] as String,
durationMs: (fields[5] as num).toInt(),
);
}
@override
void write(BinaryWriter writer, ConnectionStat obj) {
writer
..writeByte(6)
..writeByte(0)
..write(obj.serverId)
..writeByte(1)
..write(obj.serverName)
..writeByte(2)
..write(obj.timestamp)
..writeByte(3)
..write(obj.result)
..writeByte(4)
..write(obj.errorMessage)
..writeByte(5)
..write(obj.durationMs);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConnectionStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ServerConnectionStatsAdapter extends TypeAdapter<ServerConnectionStats> {
@override
final typeId = 101;
@override
ServerConnectionStats read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerConnectionStats(
serverId: fields[0] as String,
serverName: fields[1] as String,
totalAttempts: (fields[2] as num).toInt(),
successCount: (fields[3] as num).toInt(),
failureCount: (fields[4] as num).toInt(),
lastSuccessTime: fields[5] == null ? null : fields[5] as DateTime?,
lastFailureTime: fields[6] == null ? null : fields[6] as DateTime?,
recentConnections: fields[7] == null
? []
: (fields[7] as List).cast<ConnectionStat>(),
successRate: (fields[8] as num).toDouble(),
);
}
@override
void write(BinaryWriter writer, ServerConnectionStats obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.serverId)
..writeByte(1)
..write(obj.serverName)
..writeByte(2)
..write(obj.totalAttempts)
..writeByte(3)
..write(obj.successCount)
..writeByte(4)
..write(obj.failureCount)
..writeByte(5)
..write(obj.lastSuccessTime)
..writeByte(6)
..write(obj.lastFailureTime)
..writeByte(7)
..write(obj.recentConnections)
..writeByte(8)
..write(obj.successRate);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerConnectionStatsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class ConnectionResultAdapter extends TypeAdapter<ConnectionResult> {
@override
final typeId = 102;
@override
ConnectionResult read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return ConnectionResult.success;
case 1:
return ConnectionResult.timeout;
case 2:
return ConnectionResult.authFailed;
case 3:
return ConnectionResult.networkError;
case 4:
return ConnectionResult.unknownError;
default:
return ConnectionResult.success;
}
}
@override
void write(BinaryWriter writer, ConnectionResult obj) {
switch (obj) {
case ConnectionResult.success:
writer.writeByte(0);
case ConnectionResult.timeout:
writer.writeByte(1);
case ConnectionResult.authFailed:
writer.writeByte(2);
case ConnectionResult.networkError:
writer.writeByte(3);
case ConnectionResult.unknownError:
writer.writeByte(4);
}
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConnectionResultAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ConnectionStat _$ConnectionStatFromJson(Map<String, dynamic> json) =>
_ConnectionStat(
serverId: json['serverId'] as String,
serverName: json['serverName'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
result: $enumDecode(_$ConnectionResultEnumMap, json['result']),
errorMessage: json['errorMessage'] as String? ?? '',
durationMs: (json['durationMs'] as num).toInt(),
);
Map<String, dynamic> _$ConnectionStatToJson(_ConnectionStat instance) =>
<String, dynamic>{
'serverId': instance.serverId,
'serverName': instance.serverName,
'timestamp': instance.timestamp.toIso8601String(),
'result': _$ConnectionResultEnumMap[instance.result]!,
'errorMessage': instance.errorMessage,
'durationMs': instance.durationMs,
};
const _$ConnectionResultEnumMap = {
ConnectionResult.success: 'success',
ConnectionResult.timeout: 'timeout',
ConnectionResult.authFailed: 'auth_failed',
ConnectionResult.networkError: 'network_error',
ConnectionResult.unknownError: 'unknown_error',
};
_ServerConnectionStats _$ServerConnectionStatsFromJson(
Map<String, dynamic> json,
) => _ServerConnectionStats(
serverId: json['serverId'] as String,
serverName: json['serverName'] as String,
totalAttempts: (json['totalAttempts'] as num).toInt(),
successCount: (json['successCount'] as num).toInt(),
failureCount: (json['failureCount'] as num).toInt(),
lastSuccessTime: json['lastSuccessTime'] == null
? null
: DateTime.parse(json['lastSuccessTime'] as String),
lastFailureTime: json['lastFailureTime'] == null
? null
: DateTime.parse(json['lastFailureTime'] as String),
recentConnections:
(json['recentConnections'] as List<dynamic>?)
?.map((e) => ConnectionStat.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
successRate: (json['successRate'] as num).toDouble(),
);
Map<String, dynamic> _$ServerConnectionStatsToJson(
_ServerConnectionStats instance,
) => <String, dynamic>{
'serverId': instance.serverId,
'serverName': instance.serverName,
'totalAttempts': instance.totalAttempts,
'successCount': instance.successCount,
'failureCount': instance.failureCount,
'lastSuccessTime': instance.lastSuccessTime?.toIso8601String(),
'lastFailureTime': instance.lastFailureTime?.toIso8601String(),
'recentConnections': instance.recentConnections,
'successRate': instance.successRate,
};

View File

@@ -6,7 +6,7 @@ part of 'container.dart';
// RiverpodGenerator
// **************************************************************************
String _$containerNotifierHash() => r'db8f8a6b6071b7b33fbf79128dfed408a5b9fdad';
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -14,6 +14,7 @@ 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';
@@ -145,6 +146,15 @@ 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,
@@ -156,6 +166,29 @@ 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

@@ -6,7 +6,7 @@ part of 'single.dart';
// RiverpodGenerator
// **************************************************************************
String _$serverNotifierHash() => r'5625b0a4762c28efdbc124809c03b84a51d213b1';
String _$serverNotifierHash() => r'524647748cc3810c17e5c1cd29e360f3936f5014';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:get_it/get_it.dart';
import 'package:server_box/data/store/connection_stats.dart';
import 'package:server_box/data/store/container.dart';
import 'package:server_box/data/store/history.dart';
import 'package:server_box/data/store/private_key.dart';
@@ -16,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>();
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
/// All stores that need backup
static List<HiveStore> get _allBackup => [
@@ -25,6 +27,7 @@ abstract final class Stores {
key,
snippet,
history,
connectionStats,
];
static Future<void> init() async {
@@ -34,6 +37,7 @@ abstract final class Stores {
getIt.registerLazySingleton<PrivateKeyStore>(() => PrivateKeyStore.instance);
getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);
getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);
getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);
await Future.wait(_allBackup.map((store) => store.init()));
}

View File

@@ -0,0 +1,190 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
class ConnectionStatsStore extends HiveStore {
ConnectionStatsStore._() : super('connection_stats');
static final instance = ConnectionStatsStore._();
// Record a connection attempt
void recordConnection(ConnectionStat stat) {
final key = '${stat.serverId}_${ShortId.generate()}';
set(key, stat);
_cleanOldRecords(stat.serverId);
}
// Clean records older than 30 days for a specific server
void _cleanOldRecords(String serverId) {
final cutoffTime = DateTime.now().subtract(const Duration(days: 30));
final allKeys = keys().toList();
final keysToDelete = <String>[];
for (final key in allKeys) {
if (key.startsWith(serverId)) {
final parts = key.split('_');
if (parts.length >= 2) {
final timestamp = int.tryParse(parts.last);
if (timestamp != null) {
final recordTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
if (recordTime.isBefore(cutoffTime)) {
keysToDelete.add(key);
}
}
}
}
}
for (final key in keysToDelete) {
remove(key);
}
}
// Get connection stats for a specific server
ServerConnectionStats getServerStats(String serverId, String serverName) {
final allStats = getConnectionHistory(serverId);
if (allStats.isEmpty) {
return ServerConnectionStats(
serverId: serverId,
serverName: serverName,
totalAttempts: 0,
successCount: 0,
failureCount: 0,
recentConnections: [],
successRate: 0.0,
);
}
final totalAttempts = allStats.length;
final successCount = allStats.where((s) => s.result.isSuccess).length;
final failureCount = totalAttempts - successCount;
final successRate = totalAttempts > 0 ? (successCount / totalAttempts) : 0.0;
final successTimes = allStats
.where((s) => s.result.isSuccess)
.map((s) => s.timestamp)
.toList();
final failureTimes = allStats
.where((s) => !s.result.isSuccess)
.map((s) => s.timestamp)
.toList();
DateTime? lastSuccessTime;
DateTime? lastFailureTime;
if (successTimes.isNotEmpty) {
successTimes.sort((a, b) => b.compareTo(a));
lastSuccessTime = successTimes.first;
}
if (failureTimes.isNotEmpty) {
failureTimes.sort((a, b) => b.compareTo(a));
lastFailureTime = failureTimes.first;
}
// Get recent connections (last 20)
final recentConnections = allStats.take(20).toList();
return ServerConnectionStats(
serverId: serverId,
serverName: serverName,
totalAttempts: totalAttempts,
successCount: successCount,
failureCount: failureCount,
lastSuccessTime: lastSuccessTime,
lastFailureTime: lastFailureTime,
recentConnections: recentConnections,
successRate: successRate,
);
}
// Get connection history for a specific server
List<ConnectionStat> getConnectionHistory(String serverId) {
final allKeys = keys().where((key) => key.startsWith(serverId)).toList();
final stats = <ConnectionStat>[];
for (final key in allKeys) {
final stat = get<ConnectionStat>(
key,
fromObj: (val) {
if (val is ConnectionStat) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
return ConnectionStat.fromJson(map as Map<String, dynamic>);
} catch (e) {
dprint('Parsing ConnectionStat from JSON', e);
}
}
return null;
},
);
if (stat != null) {
stats.add(stat);
}
}
// Sort by timestamp, newest first
stats.sort((a, b) => b.timestamp.compareTo(a.timestamp));
return stats;
}
// Get all servers' stats
List<ServerConnectionStats> getAllServerStats() {
final serverIds = <String>{};
final serverNames = <String, String>{};
// Get all unique server IDs
for (final key in keys()) {
final parts = key.split('_');
if (parts.length >= 2) {
final serverId = parts[0];
serverIds.add(serverId);
// Try to get server name from the stored stat
final stat = get<ConnectionStat>(
key,
fromObj: (val) {
if (val is ConnectionStat) return val;
if (val is Map<dynamic, dynamic>) {
final map = val.toStrDynMap;
if (map == null) return null;
try {
return ConnectionStat.fromJson(map as Map<String, dynamic>);
} catch (e) {
dprint('Parsing ConnectionStat from JSON', e);
}
}
return null;
},
);
if (stat != null) {
serverNames[serverId] = stat.serverName;
}
}
}
final allStats = <ServerConnectionStats>[];
for (final serverId in serverIds) {
final serverName = serverNames[serverId] ?? serverId;
final stats = getServerStats(serverId, serverName);
allStats.add(stats);
}
return allStats;
}
// Clear all connection stats
void clearAll() {
box.clear();
}
// Clear stats for a specific server
void clearServerStats(String serverId) {
final keysToDelete = keys().where((key) => key.startsWith(serverId)).toList();
for (final key in keysToDelete) {
remove(key);
}
}
}