diff --git a/lib/data/model/app/server_detail_card.dart b/lib/data/model/app/server_detail_card.dart index 322a7ce8..b6876529 100644 --- a/lib/data/model/app/server_detail_card.dart +++ b/lib/data/model/app/server_detail_card.dart @@ -11,13 +11,13 @@ enum ServerDetailCards { swap(Icons.swap_horiz), gpu(Bootstrap.gpu_card), disk(Bootstrap.device_hdd_fill), + smart(Icons.health_and_safety, sinceBuild: 1174), net(ZondIcons.network), sensor(MingCute.dashboard_4_line), temp(FontAwesome.temperature_empty_solid), battery(Icons.battery_full), pve(BoxIcons.bxs_dashboard, sinceBuild: 818), - custom(Icons.code, sinceBuild: 825), - ; + custom(Icons.code, sinceBuild: 825); final int? sinceBuild; @@ -31,19 +31,20 @@ enum ServerDetailCards { static final names = values.map((e) => e.name).toList(); String get toStr => switch (this) { - about => libL10n.about, - cpu => 'CPU', - mem => 'RAM', - swap => 'Swap', - gpu => 'GPU', - disk => l10n.disk, - net => l10n.net, - sensor => l10n.sensors, - temp => l10n.temperature, - battery => l10n.battery, - pve => 'PVE', - custom => l10n.cmd, - }; + about => libL10n.about, + cpu => 'CPU', + mem => 'RAM', + swap => 'Swap', + gpu => 'GPU', + disk => l10n.disk, + smart => l10n.diskHealth, + net => l10n.net, + sensor => l10n.sensors, + temp => l10n.temperature, + battery => l10n.battery, + pve => 'PVE', + custom => l10n.cmd, + }; /// If: /// Version 1 => user set [about], default is [about, cpu] diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index 63bfbb94..bbdf4e2a 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -9,8 +9,7 @@ enum ShellFunc { process, shutdown, reboot, - suspend, - ; + suspend; static const seperator = 'SrvBoxSep'; @@ -29,7 +28,9 @@ enum ShellFunc { /// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible, /// it will be changed to [scriptDirHome]/[scriptFile]. static String getScriptDir(String id) { - final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir; + final customScriptDir = ServerProvider.pick( + id: id, + )?.value.spi.custom?.scriptDir; if (customScriptDir != null) return customScriptDir; return _scriptDirMap.putIfAbsent(id, () { return scriptDirTmp; @@ -37,10 +38,10 @@ enum ShellFunc { } static void switchScriptDir(String id) => switch (_scriptDirMap[id]) { - scriptDirTmp => _scriptDirMap[id] = scriptDirHome, - scriptDirHome => _scriptDirMap[id] = scriptDirTmp, - _ => _scriptDirMap[id] = scriptDirHome, - }; + scriptDirTmp => _scriptDirMap[id] = scriptDirHome, + scriptDirHome => _scriptDirMap[id] = scriptDirTmp, + _ => _scriptDirMap[id] = scriptDirHome, + }; static String getScriptPath(String id) { return '${getScriptDir(id)}/$scriptFile'; @@ -57,13 +58,13 @@ chmod 755 $scriptPath } String get flag => switch (this) { - ShellFunc.process => 'p', - ShellFunc.shutdown => 'sd', - ShellFunc.reboot => 'r', - ShellFunc.suspend => 'sp', - ShellFunc.status => 's', - // ShellFunc.docker=> 'd', - }; + ShellFunc.process => 'p', + ShellFunc.shutdown => 'sd', + ShellFunc.reboot => 'r', + ShellFunc.suspend => 'sp', + ShellFunc.status => 's', + // ShellFunc.docker=> 'd', + }; String exec(String id) => 'sh ${getScriptPath(id)} -$flag'; @@ -94,14 +95,14 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then else \t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)} fi'''; -// case ShellFunc.docker: -// return ''' -// result=\$(docker version 2>&1 | grep "permission denied") -// if [ "\$result" != "" ]; then -// \t${_dockerCmds.join(_cmdDivider)} -// else -// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)} -// fi'''; + // case ShellFunc.docker: + // return ''' + // result=\$(docker version 2>&1 | grep "permission denied") + // if [ "\$result" != "" ]; then + // \t${_dockerCmds.join(_cmdDivider)} + // else + // \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)} + // fi'''; case ShellFunc.process: return ''' if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then @@ -162,7 +163,9 @@ exec 2>/dev/null // Write each func for (final func in values) { final customCmdsStr = () { - if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) { + if (func == ShellFunc.status && + customCmds != null && + customCmds.isNotEmpty) { return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}'; } return ''; @@ -209,17 +212,21 @@ enum StatusCmdType { cpu._('cat /proc/stat | grep cpu'), uptime._('uptime'), conn._('cat /proc/net/snmp'), - disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'), + disk._( + 'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID', + ), mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"), tempType._('cat /sys/class/thermal/thermal_zone*/type'), tempVal._('cat /sys/class/thermal/thermal_zone*/temp'), host._('cat /etc/hostname'), diskio._('cat /proc/diskstats'), - battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'), + battery._( + 'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done', + ), nvidia._('nvidia-smi -q -x'), sensors._('sensors'), - cpuBrand._('cat /proc/cpuinfo | grep "model name"'), - ; + diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -j /dev/\$d; echo; done'), + cpuBrand._('cat /proc/cpuinfo | grep "model name"'); final String cmd; @@ -238,8 +245,7 @@ enum BSDStatusCmdType { mem._('top -l 1 | grep PhysMem'), //temp, host._('hostname'), - cpuBrand._('sysctl -n machdep.cpu.brand_string'), - ; + cpuBrand._('sysctl -n machdep.cpu.brand_string'); final String cmd; @@ -248,10 +254,10 @@ enum BSDStatusCmdType { extension StatusCmdTypeX on StatusCmdType { String get i18n => switch (this) { - StatusCmdType.sys => l10n.system, - StatusCmdType.host => l10n.host, - StatusCmdType.uptime => l10n.uptime, - StatusCmdType.battery => l10n.battery, - final val => val.name, - }; + StatusCmdType.sys => l10n.system, + StatusCmdType.host => l10n.host, + StatusCmdType.uptime => l10n.uptime, + StatusCmdType.battery => l10n.battery, + final val => val.name, + }; } diff --git a/lib/data/model/server/disk_smart.dart b/lib/data/model/server/disk_smart.dart new file mode 100644 index 00000000..fcfef991 --- /dev/null +++ b/lib/data/model/server/disk_smart.dart @@ -0,0 +1,204 @@ +import 'dart:convert'; + +import 'package:fl_lib/fl_lib.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'disk_smart.freezed.dart'; +part 'disk_smart.g.dart'; + +@freezed +class DiskSmart with _$DiskSmart { + const DiskSmart._(); + + const factory DiskSmart({ + required String device, + bool? healthy, + double? temperature, + String? model, + String? serial, + int? powerOnHours, + int? powerCycleCount, + required Map rawData, + required Map smartAttributes, + }) = _DiskSmart; + + factory DiskSmart.fromJson(Map json) => _$DiskSmartFromJson(json); + + static List parse(String raw) { + final results = []; + + final jsonBlocks = raw.split('\n\n').where((s) => s.trim().isNotEmpty); + + for (final jsonStr in jsonBlocks) { + try { + final data = json.decode(jsonStr.trim()) as Map; + + // Basic + final device = data['device']?['name']?.toString() ?? ''; + final healthy = data['smart_status']?['passed'] as bool?; + + // Model and Serial + final model = + data['model_name']?.toString() ?? + data['model_family']?.toString() ?? + data['device']?['model_name']?.toString(); + final serial = data['serial_number']?.toString() ?? data['device']?['serial_number']?.toString(); + + // SMART Attrs + final smartAttributes = _parseSmartAttributes(data); + final temperature = _extractTemperature(data, smartAttributes); + final powerOnHours = + data['power_on_time']?['hours'] as int? ?? smartAttributes['Power_On_Hours']?.rawValue as int?; + final powerCycleCount = + data['power_cycle_count'] as int? ?? smartAttributes['Power_Cycle_Count']?.rawValue as int?; + + results.add( + DiskSmart( + device: device, + healthy: healthy, + temperature: temperature, + model: model, + serial: serial, + powerOnHours: powerOnHours, + powerCycleCount: powerCycleCount, + rawData: data, + smartAttributes: smartAttributes, + ), + ); + } catch (e, s) { + Loggers.app.warning('DiskSmart parse', e, s); + } + } + return results; + } + + static Map _parseSmartAttributes(Map data) { + final attributes = {}; + + final attrTable = data['ata_smart_attributes']?['table'] as List?; + if (attrTable == null) return attributes; + + for (final attr in attrTable) { + if (attr is Map) { + final name = attr['name']?.toString(); + if (name != null) { + attributes[name] = SmartAttribute( + id: attr['id'] as int?, + name: name, + value: attr['value'] as int?, + worst: attr['worst'] as int?, + thresh: attr['thresh'] as int?, + whenFailed: attr['when_failed']?.toString(), + rawValue: attr['raw']?['value'], + rawString: attr['raw']?['string']?.toString(), + flags: SmartAttributeFlags.fromMap(attr['flags'] as Map? ?? {}), + ); + } + } + } + + return attributes; + } + + static final _tempReg = RegExp(r'^(\d+(?:\.\d+)?)'); + + /// Extract temperature from the data + static double? _extractTemperature(Map data, Map attrs) { + // Directly + final directTemp = data['temperature']?['current']; + if (directTemp is num) return directTemp.toDouble(); + + // SMART attribute + final tempAttr = attrs['Temperature_Celsius']; + if (tempAttr != null) { + // "35 (Min/Max 14/61)" + final rawString = tempAttr.rawString; + if (rawString != null) { + final match = _tempReg.firstMatch(rawString); + if (match != null) { + return double.tryParse(match.group(1)!); + } + } + + // Simple numeric value + if (tempAttr.rawValue is num && tempAttr.rawValue! < 150) { + return tempAttr.rawValue!.toDouble(); + } + } + + return null; + } + + /// Get the specific SMART attribute by name + SmartAttribute? getAttribute(String name) => smartAttributes[name]; + + int? get ssdLifeLeft => smartAttributes['SSD_Life_Left']?.rawValue as int?; + int? get lifetimeWritesGiB => smartAttributes['Lifetime_Writes_GiB']?.rawValue as int?; + int? get lifetimeReadsGiB => smartAttributes['Lifetime_Reads_GiB']?.rawValue as int?; + int? get unsafeShutdownCount => smartAttributes['Unsafe_Shutdown_Count']?.rawValue as int?; + int? get averageEraseCount => smartAttributes['Average_Erase_Count']?.rawValue as int?; + int? get maxEraseCount => smartAttributes['Max_Erase_Count']?.rawValue as int?; + + @override + String toString() => 'DiskSmart($device)'; +} + +@freezed +class SmartAttribute with _$SmartAttribute { + const SmartAttribute._(); + + const factory SmartAttribute({ + int? id, + required String name, + int? value, + int? worst, + int? thresh, + String? whenFailed, + dynamic rawValue, + String? rawString, + required SmartAttributeFlags flags, + }) = _SmartAttribute; + + factory SmartAttribute.fromJson(Map json) => _$SmartAttributeFromJson(json); + + @override + String toString() { + return 'SmartAttribute(id: $id, name: $name)'; + } +} + +@freezed +class SmartAttributeFlags with _$SmartAttributeFlags { + const SmartAttributeFlags._(); + + const factory SmartAttributeFlags({ + int? value, + String? string, + @Default(false) bool prefailure, + @Default(false) bool updatedOnline, + @Default(false) bool performance, + @Default(false) bool errorRate, + @Default(false) bool eventCount, + @Default(false) bool autoKeep, + }) = _SmartAttributeFlags; + + factory SmartAttributeFlags.fromJson(Map json) => _$SmartAttributeFlagsFromJson(json); + + factory SmartAttributeFlags.fromMap(Map map) { + return SmartAttributeFlags( + value: map['value'] as int?, + string: map['string']?.toString(), + prefailure: map['prefailure'] == true, + updatedOnline: map['updated_online'] == true, + performance: map['performance'] == true, + errorRate: map['error_rate'] == true, + eventCount: map['event_count'] == true, + autoKeep: map['auto_keep'] == true, + ); + } + + @override + String toString() { + return 'SmartAttributeFlags(value: $value, string: $string)'; + } +} diff --git a/lib/data/model/server/disk_smart.freezed.dart b/lib/data/model/server/disk_smart.freezed.dart new file mode 100644 index 00000000..724a88b0 --- /dev/null +++ b/lib/data/model/server/disk_smart.freezed.dart @@ -0,0 +1,1038 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// 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 'disk_smart.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +DiskSmart _$DiskSmartFromJson(Map json) { + return _DiskSmart.fromJson(json); +} + +/// @nodoc +mixin _$DiskSmart { + String get device => throw _privateConstructorUsedError; + bool? get healthy => throw _privateConstructorUsedError; + double? get temperature => throw _privateConstructorUsedError; + String? get model => throw _privateConstructorUsedError; + String? get serial => throw _privateConstructorUsedError; + int? get powerOnHours => throw _privateConstructorUsedError; + int? get powerCycleCount => throw _privateConstructorUsedError; + Map get rawData => throw _privateConstructorUsedError; + Map get smartAttributes => + throw _privateConstructorUsedError; + + /// Serializes this DiskSmart to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of DiskSmart + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DiskSmartCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DiskSmartCopyWith<$Res> { + factory $DiskSmartCopyWith(DiskSmart value, $Res Function(DiskSmart) then) = + _$DiskSmartCopyWithImpl<$Res, DiskSmart>; + @useResult + $Res call({ + String device, + bool? healthy, + double? temperature, + String? model, + String? serial, + int? powerOnHours, + int? powerCycleCount, + Map rawData, + Map smartAttributes, + }); +} + +/// @nodoc +class _$DiskSmartCopyWithImpl<$Res, $Val extends DiskSmart> + implements $DiskSmartCopyWith<$Res> { + _$DiskSmartCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DiskSmart + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? device = null, + Object? healthy = freezed, + Object? temperature = freezed, + Object? model = freezed, + Object? serial = freezed, + Object? powerOnHours = freezed, + Object? powerCycleCount = freezed, + Object? rawData = null, + Object? smartAttributes = null, + }) { + return _then( + _value.copyWith( + device: null == device + ? _value.device + : device // ignore: cast_nullable_to_non_nullable + as String, + healthy: freezed == healthy + ? _value.healthy + : healthy // ignore: cast_nullable_to_non_nullable + as bool?, + temperature: freezed == temperature + ? _value.temperature + : temperature // ignore: cast_nullable_to_non_nullable + as double?, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + serial: freezed == serial + ? _value.serial + : serial // ignore: cast_nullable_to_non_nullable + as String?, + powerOnHours: freezed == powerOnHours + ? _value.powerOnHours + : powerOnHours // ignore: cast_nullable_to_non_nullable + as int?, + powerCycleCount: freezed == powerCycleCount + ? _value.powerCycleCount + : powerCycleCount // ignore: cast_nullable_to_non_nullable + as int?, + rawData: null == rawData + ? _value.rawData + : rawData // ignore: cast_nullable_to_non_nullable + as Map, + smartAttributes: null == smartAttributes + ? _value.smartAttributes + : smartAttributes // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$DiskSmartImplCopyWith<$Res> + implements $DiskSmartCopyWith<$Res> { + factory _$$DiskSmartImplCopyWith( + _$DiskSmartImpl value, + $Res Function(_$DiskSmartImpl) then, + ) = __$$DiskSmartImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String device, + bool? healthy, + double? temperature, + String? model, + String? serial, + int? powerOnHours, + int? powerCycleCount, + Map rawData, + Map smartAttributes, + }); +} + +/// @nodoc +class __$$DiskSmartImplCopyWithImpl<$Res> + extends _$DiskSmartCopyWithImpl<$Res, _$DiskSmartImpl> + implements _$$DiskSmartImplCopyWith<$Res> { + __$$DiskSmartImplCopyWithImpl( + _$DiskSmartImpl _value, + $Res Function(_$DiskSmartImpl) _then, + ) : super(_value, _then); + + /// Create a copy of DiskSmart + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? device = null, + Object? healthy = freezed, + Object? temperature = freezed, + Object? model = freezed, + Object? serial = freezed, + Object? powerOnHours = freezed, + Object? powerCycleCount = freezed, + Object? rawData = null, + Object? smartAttributes = null, + }) { + return _then( + _$DiskSmartImpl( + device: null == device + ? _value.device + : device // ignore: cast_nullable_to_non_nullable + as String, + healthy: freezed == healthy + ? _value.healthy + : healthy // ignore: cast_nullable_to_non_nullable + as bool?, + temperature: freezed == temperature + ? _value.temperature + : temperature // ignore: cast_nullable_to_non_nullable + as double?, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + serial: freezed == serial + ? _value.serial + : serial // ignore: cast_nullable_to_non_nullable + as String?, + powerOnHours: freezed == powerOnHours + ? _value.powerOnHours + : powerOnHours // ignore: cast_nullable_to_non_nullable + as int?, + powerCycleCount: freezed == powerCycleCount + ? _value.powerCycleCount + : powerCycleCount // ignore: cast_nullable_to_non_nullable + as int?, + rawData: null == rawData + ? _value._rawData + : rawData // ignore: cast_nullable_to_non_nullable + as Map, + smartAttributes: null == smartAttributes + ? _value._smartAttributes + : smartAttributes // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$DiskSmartImpl extends _DiskSmart { + const _$DiskSmartImpl({ + required this.device, + this.healthy, + this.temperature, + this.model, + this.serial, + this.powerOnHours, + this.powerCycleCount, + required final Map rawData, + required final Map smartAttributes, + }) : _rawData = rawData, + _smartAttributes = smartAttributes, + super._(); + + factory _$DiskSmartImpl.fromJson(Map json) => + _$$DiskSmartImplFromJson(json); + + @override + final String device; + @override + final bool? healthy; + @override + final double? temperature; + @override + final String? model; + @override + final String? serial; + @override + final int? powerOnHours; + @override + final int? powerCycleCount; + final Map _rawData; + @override + Map get rawData { + if (_rawData is EqualUnmodifiableMapView) return _rawData; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_rawData); + } + + final Map _smartAttributes; + @override + Map get smartAttributes { + if (_smartAttributes is EqualUnmodifiableMapView) return _smartAttributes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_smartAttributes); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DiskSmartImpl && + (identical(other.device, device) || other.device == device) && + (identical(other.healthy, healthy) || other.healthy == healthy) && + (identical(other.temperature, temperature) || + other.temperature == temperature) && + (identical(other.model, model) || other.model == model) && + (identical(other.serial, serial) || other.serial == serial) && + (identical(other.powerOnHours, powerOnHours) || + other.powerOnHours == powerOnHours) && + (identical(other.powerCycleCount, powerCycleCount) || + other.powerCycleCount == powerCycleCount) && + const DeepCollectionEquality().equals(other._rawData, _rawData) && + const DeepCollectionEquality().equals( + other._smartAttributes, + _smartAttributes, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + device, + healthy, + temperature, + model, + serial, + powerOnHours, + powerCycleCount, + const DeepCollectionEquality().hash(_rawData), + const DeepCollectionEquality().hash(_smartAttributes), + ); + + /// Create a copy of DiskSmart + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DiskSmartImplCopyWith<_$DiskSmartImpl> get copyWith => + __$$DiskSmartImplCopyWithImpl<_$DiskSmartImpl>(this, _$identity); + + @override + Map toJson() { + return _$$DiskSmartImplToJson(this); + } +} + +abstract class _DiskSmart extends DiskSmart { + const factory _DiskSmart({ + required final String device, + final bool? healthy, + final double? temperature, + final String? model, + final String? serial, + final int? powerOnHours, + final int? powerCycleCount, + required final Map rawData, + required final Map smartAttributes, + }) = _$DiskSmartImpl; + const _DiskSmart._() : super._(); + + factory _DiskSmart.fromJson(Map json) = + _$DiskSmartImpl.fromJson; + + @override + String get device; + @override + bool? get healthy; + @override + double? get temperature; + @override + String? get model; + @override + String? get serial; + @override + int? get powerOnHours; + @override + int? get powerCycleCount; + @override + Map get rawData; + @override + Map get smartAttributes; + + /// Create a copy of DiskSmart + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DiskSmartImplCopyWith<_$DiskSmartImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SmartAttribute _$SmartAttributeFromJson(Map json) { + return _SmartAttribute.fromJson(json); +} + +/// @nodoc +mixin _$SmartAttribute { + int? get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + int? get value => throw _privateConstructorUsedError; + int? get worst => throw _privateConstructorUsedError; + int? get thresh => throw _privateConstructorUsedError; + String? get whenFailed => throw _privateConstructorUsedError; + dynamic get rawValue => throw _privateConstructorUsedError; + String? get rawString => throw _privateConstructorUsedError; + SmartAttributeFlags get flags => throw _privateConstructorUsedError; + + /// Serializes this SmartAttribute to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SmartAttribute + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SmartAttributeCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SmartAttributeCopyWith<$Res> { + factory $SmartAttributeCopyWith( + SmartAttribute value, + $Res Function(SmartAttribute) then, + ) = _$SmartAttributeCopyWithImpl<$Res, SmartAttribute>; + @useResult + $Res call({ + int? id, + String name, + int? value, + int? worst, + int? thresh, + String? whenFailed, + dynamic rawValue, + String? rawString, + SmartAttributeFlags flags, + }); + + $SmartAttributeFlagsCopyWith<$Res> get flags; +} + +/// @nodoc +class _$SmartAttributeCopyWithImpl<$Res, $Val extends SmartAttribute> + implements $SmartAttributeCopyWith<$Res> { + _$SmartAttributeCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SmartAttribute + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = null, + Object? value = freezed, + Object? worst = freezed, + Object? thresh = freezed, + Object? whenFailed = freezed, + Object? rawValue = freezed, + Object? rawString = freezed, + Object? flags = null, + }) { + return _then( + _value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + value: freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as int?, + worst: freezed == worst + ? _value.worst + : worst // ignore: cast_nullable_to_non_nullable + as int?, + thresh: freezed == thresh + ? _value.thresh + : thresh // ignore: cast_nullable_to_non_nullable + as int?, + whenFailed: freezed == whenFailed + ? _value.whenFailed + : whenFailed // ignore: cast_nullable_to_non_nullable + as String?, + rawValue: freezed == rawValue + ? _value.rawValue + : rawValue // ignore: cast_nullable_to_non_nullable + as dynamic, + rawString: freezed == rawString + ? _value.rawString + : rawString // ignore: cast_nullable_to_non_nullable + as String?, + flags: null == flags + ? _value.flags + : flags // ignore: cast_nullable_to_non_nullable + as SmartAttributeFlags, + ) + as $Val, + ); + } + + /// Create a copy of SmartAttribute + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SmartAttributeFlagsCopyWith<$Res> get flags { + return $SmartAttributeFlagsCopyWith<$Res>(_value.flags, (value) { + return _then(_value.copyWith(flags: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$SmartAttributeImplCopyWith<$Res> + implements $SmartAttributeCopyWith<$Res> { + factory _$$SmartAttributeImplCopyWith( + _$SmartAttributeImpl value, + $Res Function(_$SmartAttributeImpl) then, + ) = __$$SmartAttributeImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int? id, + String name, + int? value, + int? worst, + int? thresh, + String? whenFailed, + dynamic rawValue, + String? rawString, + SmartAttributeFlags flags, + }); + + @override + $SmartAttributeFlagsCopyWith<$Res> get flags; +} + +/// @nodoc +class __$$SmartAttributeImplCopyWithImpl<$Res> + extends _$SmartAttributeCopyWithImpl<$Res, _$SmartAttributeImpl> + implements _$$SmartAttributeImplCopyWith<$Res> { + __$$SmartAttributeImplCopyWithImpl( + _$SmartAttributeImpl _value, + $Res Function(_$SmartAttributeImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SmartAttribute + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? name = null, + Object? value = freezed, + Object? worst = freezed, + Object? thresh = freezed, + Object? whenFailed = freezed, + Object? rawValue = freezed, + Object? rawString = freezed, + Object? flags = null, + }) { + return _then( + _$SmartAttributeImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + value: freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as int?, + worst: freezed == worst + ? _value.worst + : worst // ignore: cast_nullable_to_non_nullable + as int?, + thresh: freezed == thresh + ? _value.thresh + : thresh // ignore: cast_nullable_to_non_nullable + as int?, + whenFailed: freezed == whenFailed + ? _value.whenFailed + : whenFailed // ignore: cast_nullable_to_non_nullable + as String?, + rawValue: freezed == rawValue + ? _value.rawValue + : rawValue // ignore: cast_nullable_to_non_nullable + as dynamic, + rawString: freezed == rawString + ? _value.rawString + : rawString // ignore: cast_nullable_to_non_nullable + as String?, + flags: null == flags + ? _value.flags + : flags // ignore: cast_nullable_to_non_nullable + as SmartAttributeFlags, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$SmartAttributeImpl extends _SmartAttribute { + const _$SmartAttributeImpl({ + this.id, + required this.name, + this.value, + this.worst, + this.thresh, + this.whenFailed, + this.rawValue, + this.rawString, + required this.flags, + }) : super._(); + + factory _$SmartAttributeImpl.fromJson(Map json) => + _$$SmartAttributeImplFromJson(json); + + @override + final int? id; + @override + final String name; + @override + final int? value; + @override + final int? worst; + @override + final int? thresh; + @override + final String? whenFailed; + @override + final dynamic rawValue; + @override + final String? rawString; + @override + final SmartAttributeFlags flags; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SmartAttributeImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.value, value) || other.value == value) && + (identical(other.worst, worst) || other.worst == worst) && + (identical(other.thresh, thresh) || other.thresh == thresh) && + (identical(other.whenFailed, whenFailed) || + other.whenFailed == whenFailed) && + const DeepCollectionEquality().equals(other.rawValue, rawValue) && + (identical(other.rawString, rawString) || + other.rawString == rawString) && + (identical(other.flags, flags) || other.flags == flags)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + value, + worst, + thresh, + whenFailed, + const DeepCollectionEquality().hash(rawValue), + rawString, + flags, + ); + + /// Create a copy of SmartAttribute + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SmartAttributeImplCopyWith<_$SmartAttributeImpl> get copyWith => + __$$SmartAttributeImplCopyWithImpl<_$SmartAttributeImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$SmartAttributeImplToJson(this); + } +} + +abstract class _SmartAttribute extends SmartAttribute { + const factory _SmartAttribute({ + final int? id, + required final String name, + final int? value, + final int? worst, + final int? thresh, + final String? whenFailed, + final dynamic rawValue, + final String? rawString, + required final SmartAttributeFlags flags, + }) = _$SmartAttributeImpl; + const _SmartAttribute._() : super._(); + + factory _SmartAttribute.fromJson(Map json) = + _$SmartAttributeImpl.fromJson; + + @override + int? get id; + @override + String get name; + @override + int? get value; + @override + int? get worst; + @override + int? get thresh; + @override + String? get whenFailed; + @override + dynamic get rawValue; + @override + String? get rawString; + @override + SmartAttributeFlags get flags; + + /// Create a copy of SmartAttribute + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SmartAttributeImplCopyWith<_$SmartAttributeImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SmartAttributeFlags _$SmartAttributeFlagsFromJson(Map json) { + return _SmartAttributeFlags.fromJson(json); +} + +/// @nodoc +mixin _$SmartAttributeFlags { + int? get value => throw _privateConstructorUsedError; + String? get string => throw _privateConstructorUsedError; + bool get prefailure => throw _privateConstructorUsedError; + bool get updatedOnline => throw _privateConstructorUsedError; + bool get performance => throw _privateConstructorUsedError; + bool get errorRate => throw _privateConstructorUsedError; + bool get eventCount => throw _privateConstructorUsedError; + bool get autoKeep => throw _privateConstructorUsedError; + + /// Serializes this SmartAttributeFlags to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SmartAttributeFlags + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SmartAttributeFlagsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SmartAttributeFlagsCopyWith<$Res> { + factory $SmartAttributeFlagsCopyWith( + SmartAttributeFlags value, + $Res Function(SmartAttributeFlags) then, + ) = _$SmartAttributeFlagsCopyWithImpl<$Res, SmartAttributeFlags>; + @useResult + $Res call({ + int? value, + String? string, + bool prefailure, + bool updatedOnline, + bool performance, + bool errorRate, + bool eventCount, + bool autoKeep, + }); +} + +/// @nodoc +class _$SmartAttributeFlagsCopyWithImpl<$Res, $Val extends SmartAttributeFlags> + implements $SmartAttributeFlagsCopyWith<$Res> { + _$SmartAttributeFlagsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SmartAttributeFlags + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? value = freezed, + Object? string = freezed, + Object? prefailure = null, + Object? updatedOnline = null, + Object? performance = null, + Object? errorRate = null, + Object? eventCount = null, + Object? autoKeep = null, + }) { + return _then( + _value.copyWith( + value: freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as int?, + string: freezed == string + ? _value.string + : string // ignore: cast_nullable_to_non_nullable + as String?, + prefailure: null == prefailure + ? _value.prefailure + : prefailure // ignore: cast_nullable_to_non_nullable + as bool, + updatedOnline: null == updatedOnline + ? _value.updatedOnline + : updatedOnline // ignore: cast_nullable_to_non_nullable + as bool, + performance: null == performance + ? _value.performance + : performance // ignore: cast_nullable_to_non_nullable + as bool, + errorRate: null == errorRate + ? _value.errorRate + : errorRate // ignore: cast_nullable_to_non_nullable + as bool, + eventCount: null == eventCount + ? _value.eventCount + : eventCount // ignore: cast_nullable_to_non_nullable + as bool, + autoKeep: null == autoKeep + ? _value.autoKeep + : autoKeep // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$SmartAttributeFlagsImplCopyWith<$Res> + implements $SmartAttributeFlagsCopyWith<$Res> { + factory _$$SmartAttributeFlagsImplCopyWith( + _$SmartAttributeFlagsImpl value, + $Res Function(_$SmartAttributeFlagsImpl) then, + ) = __$$SmartAttributeFlagsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int? value, + String? string, + bool prefailure, + bool updatedOnline, + bool performance, + bool errorRate, + bool eventCount, + bool autoKeep, + }); +} + +/// @nodoc +class __$$SmartAttributeFlagsImplCopyWithImpl<$Res> + extends _$SmartAttributeFlagsCopyWithImpl<$Res, _$SmartAttributeFlagsImpl> + implements _$$SmartAttributeFlagsImplCopyWith<$Res> { + __$$SmartAttributeFlagsImplCopyWithImpl( + _$SmartAttributeFlagsImpl _value, + $Res Function(_$SmartAttributeFlagsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SmartAttributeFlags + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? value = freezed, + Object? string = freezed, + Object? prefailure = null, + Object? updatedOnline = null, + Object? performance = null, + Object? errorRate = null, + Object? eventCount = null, + Object? autoKeep = null, + }) { + return _then( + _$SmartAttributeFlagsImpl( + value: freezed == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as int?, + string: freezed == string + ? _value.string + : string // ignore: cast_nullable_to_non_nullable + as String?, + prefailure: null == prefailure + ? _value.prefailure + : prefailure // ignore: cast_nullable_to_non_nullable + as bool, + updatedOnline: null == updatedOnline + ? _value.updatedOnline + : updatedOnline // ignore: cast_nullable_to_non_nullable + as bool, + performance: null == performance + ? _value.performance + : performance // ignore: cast_nullable_to_non_nullable + as bool, + errorRate: null == errorRate + ? _value.errorRate + : errorRate // ignore: cast_nullable_to_non_nullable + as bool, + eventCount: null == eventCount + ? _value.eventCount + : eventCount // ignore: cast_nullable_to_non_nullable + as bool, + autoKeep: null == autoKeep + ? _value.autoKeep + : autoKeep // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$SmartAttributeFlagsImpl extends _SmartAttributeFlags { + const _$SmartAttributeFlagsImpl({ + this.value, + this.string, + this.prefailure = false, + this.updatedOnline = false, + this.performance = false, + this.errorRate = false, + this.eventCount = false, + this.autoKeep = false, + }) : super._(); + + factory _$SmartAttributeFlagsImpl.fromJson(Map json) => + _$$SmartAttributeFlagsImplFromJson(json); + + @override + final int? value; + @override + final String? string; + @override + @JsonKey() + final bool prefailure; + @override + @JsonKey() + final bool updatedOnline; + @override + @JsonKey() + final bool performance; + @override + @JsonKey() + final bool errorRate; + @override + @JsonKey() + final bool eventCount; + @override + @JsonKey() + final bool autoKeep; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SmartAttributeFlagsImpl && + (identical(other.value, value) || other.value == value) && + (identical(other.string, string) || other.string == string) && + (identical(other.prefailure, prefailure) || + other.prefailure == prefailure) && + (identical(other.updatedOnline, updatedOnline) || + other.updatedOnline == updatedOnline) && + (identical(other.performance, performance) || + other.performance == performance) && + (identical(other.errorRate, errorRate) || + other.errorRate == errorRate) && + (identical(other.eventCount, eventCount) || + other.eventCount == eventCount) && + (identical(other.autoKeep, autoKeep) || + other.autoKeep == autoKeep)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + value, + string, + prefailure, + updatedOnline, + performance, + errorRate, + eventCount, + autoKeep, + ); + + /// Create a copy of SmartAttributeFlags + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SmartAttributeFlagsImplCopyWith<_$SmartAttributeFlagsImpl> get copyWith => + __$$SmartAttributeFlagsImplCopyWithImpl<_$SmartAttributeFlagsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$SmartAttributeFlagsImplToJson(this); + } +} + +abstract class _SmartAttributeFlags extends SmartAttributeFlags { + const factory _SmartAttributeFlags({ + final int? value, + final String? string, + final bool prefailure, + final bool updatedOnline, + final bool performance, + final bool errorRate, + final bool eventCount, + final bool autoKeep, + }) = _$SmartAttributeFlagsImpl; + const _SmartAttributeFlags._() : super._(); + + factory _SmartAttributeFlags.fromJson(Map json) = + _$SmartAttributeFlagsImpl.fromJson; + + @override + int? get value; + @override + String? get string; + @override + bool get prefailure; + @override + bool get updatedOnline; + @override + bool get performance; + @override + bool get errorRate; + @override + bool get eventCount; + @override + bool get autoKeep; + + /// Create a copy of SmartAttributeFlags + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SmartAttributeFlagsImplCopyWith<_$SmartAttributeFlagsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/model/server/disk_smart.g.dart b/lib/data/model/server/disk_smart.g.dart new file mode 100644 index 00000000..4cb0dc17 --- /dev/null +++ b/lib/data/model/server/disk_smart.g.dart @@ -0,0 +1,91 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'disk_smart.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$DiskSmartImpl _$$DiskSmartImplFromJson(Map json) => + _$DiskSmartImpl( + device: json['device'] as String, + healthy: json['healthy'] as bool?, + temperature: (json['temperature'] as num?)?.toDouble(), + model: json['model'] as String?, + serial: json['serial'] as String?, + powerOnHours: (json['powerOnHours'] as num?)?.toInt(), + powerCycleCount: (json['powerCycleCount'] as num?)?.toInt(), + rawData: json['rawData'] as Map, + smartAttributes: (json['smartAttributes'] as Map).map( + (k, e) => + MapEntry(k, SmartAttribute.fromJson(e as Map)), + ), + ); + +Map _$$DiskSmartImplToJson(_$DiskSmartImpl instance) => + { + 'device': instance.device, + 'healthy': instance.healthy, + 'temperature': instance.temperature, + 'model': instance.model, + 'serial': instance.serial, + 'powerOnHours': instance.powerOnHours, + 'powerCycleCount': instance.powerCycleCount, + 'rawData': instance.rawData, + 'smartAttributes': instance.smartAttributes, + }; + +_$SmartAttributeImpl _$$SmartAttributeImplFromJson(Map json) => + _$SmartAttributeImpl( + id: (json['id'] as num?)?.toInt(), + name: json['name'] as String, + value: (json['value'] as num?)?.toInt(), + worst: (json['worst'] as num?)?.toInt(), + thresh: (json['thresh'] as num?)?.toInt(), + whenFailed: json['whenFailed'] as String?, + rawValue: json['rawValue'], + rawString: json['rawString'] as String?, + flags: SmartAttributeFlags.fromJson( + json['flags'] as Map, + ), + ); + +Map _$$SmartAttributeImplToJson( + _$SmartAttributeImpl instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'value': instance.value, + 'worst': instance.worst, + 'thresh': instance.thresh, + 'whenFailed': instance.whenFailed, + 'rawValue': instance.rawValue, + 'rawString': instance.rawString, + 'flags': instance.flags, +}; + +_$SmartAttributeFlagsImpl _$$SmartAttributeFlagsImplFromJson( + Map json, +) => _$SmartAttributeFlagsImpl( + value: (json['value'] as num?)?.toInt(), + string: json['string'] as String?, + prefailure: json['prefailure'] as bool? ?? false, + updatedOnline: json['updatedOnline'] as bool? ?? false, + performance: json['performance'] as bool? ?? false, + errorRate: json['errorRate'] as bool? ?? false, + eventCount: json['eventCount'] as bool? ?? false, + autoKeep: json['autoKeep'] as bool? ?? false, +); + +Map _$$SmartAttributeFlagsImplToJson( + _$SmartAttributeFlagsImpl instance, +) => { + 'value': instance.value, + 'string': instance.string, + 'prefailure': instance.prefailure, + 'updatedOnline': instance.updatedOnline, + 'performance': instance.performance, + 'errorRate': instance.errorRate, + 'eventCount': instance.eventCount, + 'autoKeep': instance.autoKeep, +}; diff --git a/lib/data/model/server/server.dart b/lib/data/model/server/server.dart index 7e597417..076bc220 100644 --- a/lib/data/model/server/server.dart +++ b/lib/data/model/server/server.dart @@ -5,6 +5,7 @@ import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/conn.dart'; import 'package:server_box/data/model/server/cpu.dart'; import 'package:server_box/data/model/server/disk.dart'; +import 'package:server_box/data/model/server/disk_smart.dart'; import 'package:server_box/data/model/server/memory.dart'; import 'package:server_box/data/model/server/net_speed.dart'; import 'package:server_box/data/model/server/nvdia.dart'; @@ -19,12 +20,7 @@ class Server { SSHClient? client; ServerConn conn; - Server( - this.spi, - this.status, - this.conn, { - this.client, - }); + Server(this.spi, this.status, this.conn, {this.client}); bool get needGenClient => conn < ServerConn.connecting; @@ -44,6 +40,7 @@ class ServerStatus { SystemType system; Err? err; DiskIO diskIO; + List diskSmart; List? nvidia; final List batteries = []; final Map more = {}; @@ -61,6 +58,7 @@ class ServerStatus { required this.temps, required this.system, required this.diskIO, + this.diskSmart = const [], this.err, this.nvidia, this.diskUsage, diff --git a/lib/data/model/server/server_private_info.dart b/lib/data/model/server/server_private_info.dart index 9e551804..bb6aa455 100644 --- a/lib/data/model/server/server_private_info.dart +++ b/lib/data/model/server/server_private_info.dart @@ -85,7 +85,9 @@ extension Spix on Spi { VNode? get jumpServer => ServerProvider.pick(id: jumpId); bool shouldReconnect(Spi old) { - return id != old.id || + return user != old.user || + ip != old.ip || + port != old.port || pwd != old.pwd || keyId != old.keyId || alterUrl != old.alterUrl || diff --git a/lib/data/model/server/server_status_update_req.dart b/lib/data/model/server/server_status_update_req.dart index fa126c67..e3c9b3ab 100644 --- a/lib/data/model/server/server_status_update_req.dart +++ b/lib/data/model/server/server_status_update_req.dart @@ -4,6 +4,7 @@ import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/conn.dart'; import 'package:server_box/data/model/server/cpu.dart'; import 'package:server_box/data/model/server/disk.dart'; +import 'package:server_box/data/model/server/disk_smart.dart'; import 'package:server_box/data/model/server/memory.dart'; import 'package:server_box/data/model/server/net_speed.dart'; import 'package:server_box/data/model/server/nvdia.dart'; @@ -37,7 +38,8 @@ Future getStatus(ServerStatusUpdateReq req) async { Future _getLinuxStatus(ServerStatusUpdateReq req) async { final segments = req.segments; - final time = int.tryParse(StatusCmdType.time.find(segments)) ?? + final time = + int.tryParse(StatusCmdType.time.find(segments)) ?? DateTime.now().millisecondsSinceEpoch ~/ 1000; try { @@ -48,9 +50,7 @@ Future _getLinuxStatus(ServerStatusUpdateReq req) async { } try { - final sys = _parseSysVer( - StatusCmdType.sys.find(segments), - ); + final sys = _parseSysVer(StatusCmdType.sys.find(segments)); if (sys != null) { req.ss.more[StatusCmdType.sys] = sys; } @@ -130,6 +130,13 @@ Future _getLinuxStatus(ServerStatusUpdateReq req) async { Loggers.app.warning(e, s); } + try { + final smarts = DiskSmart.parse(StatusCmdType.diskSmart.find(segments)); + req.ss.diskSmart = smarts; + } catch (e, s) { + Loggers.app.warning(e, s); + } + try { req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments)); } catch (e, s) { diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index ee0a6d0f..58049616 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -45,21 +45,20 @@ class ServerProvider extends Provider { for (int idx = 0; idx < spis.length; idx++) { final spi = spis[idx]; final originServer = oldServers[spi.id]; - final newServer = genServer(spi); /// #258 /// If not [shouldReconnect], then keep the old state. if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) { - newServer.conn = originServer.value.conn; + originServer.value.spi = spi; + servers[spi.id] = originServer; + } else { + final newServer = genServer(spi); + servers[spi.id] = newServer.vn; } - servers[spi.id] = newServer.vn; } final serverOrder_ = Stores.setting.serverOrder.fetch(); if (serverOrder_.isNotEmpty) { - spis.reorder( - order: serverOrder_, - finder: (n, id) => n.id == id, - ); + spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id); serverOrder.value.addAll(spis.map((e) => e.id)); } else { serverOrder.value.addAll(servers.keys); @@ -104,31 +103,30 @@ class ServerProvider extends Provider { /// if [spi] is specificed then only refresh this server /// [onlyFailed] only refresh failed servers - static Future refresh({ - Spi? spi, - bool onlyFailed = false, - }) async { + static Future refresh({Spi? spi, bool onlyFailed = false}) async { if (spi != null) { _manualDisconnectedIds.remove(spi.id); await _getData(spi); return; } - await Future.wait(servers.values.map((val) async { - final s = val.value; - if (onlyFailed) { - if (s.conn != ServerConn.failed) return; - TryLimiter.reset(s.spi.id); - } + await Future.wait( + servers.values.map((val) async { + final s = val.value; + if (onlyFailed) { + if (s.conn != ServerConn.failed) return; + TryLimiter.reset(s.spi.id); + } - if (_manualDisconnectedIds.contains(s.spi.id)) return; + if (_manualDisconnectedIds.contains(s.spi.id)) return; - if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) { - return; - } + if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) { + return; + } - return await _getData(s.spi); - })); + return await _getData(s.spi); + }), + ); } static Future startAutoRefresh() async { @@ -307,14 +305,11 @@ class ServerProvider extends Provider { _setServerState(s, ServerConn.connected); try { - final (_, writeScriptResult) = await sv.client!.exec( - (session) async { - final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List; - session.stdin.add(scriptRaw); - session.stdin.close(); - }, - entry: ShellFunc.getInstallShellCmd(spi.id), - ); + final (_, writeScriptResult) = await sv.client!.exec((session) async { + final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List; + session.stdin.add(scriptRaw); + session.stdin.close(); + }, entry: ShellFunc.getInstallShellCmd(spi.id)); if (writeScriptResult.isNotEmpty) { ShellFunc.switchScriptDir(spi.id); throw writeScriptResult; @@ -366,10 +361,7 @@ class ServerProvider extends Provider { } } TryLimiter.inc(sid); - sv.status.err = SSHErr( - type: SSHErrType.segements, - message: 'Seperate segments failed, raw:\n$raw', - ); + sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw'); _setServerState(s, ServerConn.failed); return; } @@ -408,17 +400,10 @@ class ServerProvider extends Provider { system: systemType, customCmds: spi.custom?.cmds ?? {}, ); - sv.status = await Computer.shared.start( - getStatus, - req, - taskName: 'StatusUpdateReq<${sv.id}>', - ); + sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>'); } catch (e, trace) { TryLimiter.inc(sid); - sv.status.err = SSHErr( - type: SSHErrType.getStatus, - message: 'Parse failed: $e\n\n$raw', - ); + sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw'); _setServerState(s, ServerConn.failed); Loggers.app.warning('Server status', e, trace); return; diff --git a/lib/data/res/github_id.dart b/lib/data/res/github_id.dart index f8c38bba..3bf94838 100644 --- a/lib/data/res/github_id.dart +++ b/lib/data/res/github_id.dart @@ -17,6 +17,11 @@ abstract final class GithubIds { 'dccif', 'mikropsoft', 'CakesTwix', + 'dsvf', + 'fei1025', + 'MasedMSD', + 'GitGitro', + 'Shin-suechtig', }; static const participants = { @@ -99,6 +104,20 @@ abstract final class GithubIds { '88484396', 'honggeigei', 'likecreep', + 'axlrose', + 'immortal521', + 'PRO-2684', + 'Xiaobao-Yang', + 'Mrhs121', + 'Fudiautobi', + 'papaj-na-wrotkach', + 'kid1412621', + 'smanx', + 'xuanyue1024', + 'RuofengX', + 'rhwong', + 'AstroEngineeer', + 'mochasweet', }; } diff --git a/lib/data/res/status.dart b/lib/data/res/status.dart index ce241882..454a3b09 100644 --- a/lib/data/res/status.dart +++ b/lib/data/res/status.dart @@ -8,56 +8,33 @@ import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/temp.dart'; abstract final class InitStatus { - static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore( - 'cpu', - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ); - static Cpus get cpus => Cpus( - [_initOneTimeCpuStatus], - [_initOneTimeCpuStatus], - ); - static NetSpeedPart get _initNetSpeedPart => NetSpeedPart( - '', - BigInt.zero, - BigInt.zero, - 0, - ); - static NetSpeed get netSpeed => NetSpeed( - [_initNetSpeedPart], - [_initNetSpeedPart], - ); + static SingleCpuCore get _initOneTimeCpuStatus => + SingleCpuCore('cpu', 0, 0, 0, 0, 0, 0, 0); + static Cpus get cpus => + Cpus([_initOneTimeCpuStatus], [_initOneTimeCpuStatus]); + static NetSpeedPart get _initNetSpeedPart => + NetSpeedPart('', BigInt.zero, BigInt.zero, 0); + static NetSpeed get netSpeed => + NetSpeed([_initNetSpeedPart], [_initNetSpeedPart]); static ServerStatus get status => ServerStatus( - cpu: cpus, - mem: const Memory( - total: 1, - free: 1, - avail: 1, - ), - disk: [ - Disk( - path: '/', - mount: '/', - usedPercent: 0, - used: BigInt.zero, - size: BigInt.one, - avail: BigInt.zero, - ) - ], - tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0), - netSpeed: netSpeed, - swap: const Swap( - total: 0, - free: 0, - cached: 0, - ), - system: SystemType.linux, - temps: Temperatures(), - diskIO: DiskIO([], []), - ); + cpu: cpus, + mem: const Memory(total: 1, free: 1, avail: 1), + disk: [ + Disk( + path: '/', + mount: '/', + usedPercent: 0, + used: BigInt.zero, + size: BigInt.one, + avail: BigInt.zero, + ), + ], + tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0), + netSpeed: netSpeed, + swap: const Swap(total: 0, free: 0, cached: 0), + system: SystemType.linux, + temps: Temperatures(), + diskIO: DiskIO([], []), + diskSmart: const [], + ); } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index bb3f8db6..82c3c545 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -335,6 +335,12 @@ abstract class AppLocalizations { /// **'Disk'** String get disk; + /// No description provided for @diskHealth. + /// + /// In en, this message translates to: + /// **'Disk Health'** + String get diskHealth; + /// No description provided for @diskIgnorePath. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index a720f3b4..f1fc1624 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -128,6 +128,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get disk => 'Festplatte'; + @override + String get diskHealth => 'Festplattengesundheit'; + @override String get diskIgnorePath => 'Pfad für Datenträger ignorieren'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 6533860f..2e1a75d3 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -127,6 +127,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get disk => 'Disk'; + @override + String get diskHealth => 'Disk Health'; + @override String get diskIgnorePath => 'Ignore path for disk'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index c04e78e2..5c0223dc 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -128,6 +128,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get disk => 'Disco'; + @override + String get diskHealth => 'Salud del disco'; + @override String get diskIgnorePath => 'Rutas de disco ignoradas'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index fb1152c0..8a371721 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -128,6 +128,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get disk => 'Disque'; + @override + String get diskHealth => 'Santé du disque'; + @override String get diskIgnorePath => 'Chemin à ignorer pour le disque'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index e55ec657..95be7aba 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -127,6 +127,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get disk => 'Disk'; + @override + String get diskHealth => 'Kesehatan disk'; + @override String get diskIgnorePath => 'Abaikan jalan untuk disk'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index 96be5328..47cdb357 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -120,6 +120,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get disk => 'ディスク'; + @override + String get diskHealth => 'ディスクの健康状態'; + @override String get diskIgnorePath => '無視されたディスクパス'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 6a172f5c..92fbc7e1 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -127,6 +127,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get disk => 'Schijf'; + @override + String get diskHealth => 'Schijfgezondheid'; + @override String get diskIgnorePath => 'Pad negeren voor schijf'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 56e4a2a7..af4cac1b 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -127,6 +127,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get disk => 'Disco'; + @override + String get diskHealth => 'Saúde do disco'; + @override String get diskIgnorePath => 'Caminhos de disco ignorados'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 1d3e0d8f..8844f471 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -127,6 +127,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get disk => 'Диск'; + @override + String get diskHealth => 'Состояние диска'; + @override String get diskIgnorePath => 'Игнорировать путь к диску'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index 15ccd30c..41a61c45 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -126,6 +126,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get disk => 'Disk'; + @override + String get diskHealth => 'Disk sağlığı'; + @override String get diskIgnorePath => 'Disk için yok sayılan yol'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index daf42f57..11f2320b 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -128,6 +128,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get disk => 'Диск'; + @override + String get diskHealth => 'Стан диска'; + @override String get diskIgnorePath => 'Ігнорувати шлях для диска'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 37d5d17d..e0eddf38 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -119,6 +119,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get disk => '磁盘'; + @override + String get diskHealth => '磁盘健康'; + @override String get diskIgnorePath => '忽略的磁盘路径'; @@ -844,6 +847,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get disk => '磁碟'; + @override + String get diskHealth => '磁碟健康'; + @override String get diskIgnorePath => '忽略的磁碟路徑'; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index fe3a7ece..e5d64805 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -36,6 +36,7 @@ "dirEmpty": "Stelle sicher, dass der Ordner leer ist.", "disconnected": "Disconnected", "disk": "Festplatte", + "diskHealth": "Festplattengesundheit", "diskIgnorePath": "Pfad für Datenträger ignorieren", "displayCpuIndex": "Zeigen Sie den CPU-Index an", "dl2Local": "Datei \"{fileName}\" herunterladen?", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8bc20b77..f3d1641d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -36,6 +36,7 @@ "dirEmpty": "Make sure the folder is empty.", "disconnected": "Disconnected", "disk": "Disk", + "diskHealth": "Disk Health", "diskIgnorePath": "Ignore path for disk", "displayCpuIndex": "Display CPU index", "dl2Local": "Download {fileName} to local?", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 8f17d026..7d9aa557 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -36,6 +36,7 @@ "dirEmpty": "Asegúrate de que el directorio esté vacío", "disconnected": "Desconectado", "disk": "Disco", + "diskHealth": "Salud del disco", "diskIgnorePath": "Rutas de disco ignoradas", "displayCpuIndex": "Muestre el índice de CPU", "dl2Local": "¿Descargar {fileName} a local?", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 769576cf..80b6c8e1 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -36,6 +36,7 @@ "dirEmpty": "Assurez-vous que le répertoire est vide.", "disconnected": "Déconnecté", "disk": "Disque", + "diskHealth": "Santé du disque", "diskIgnorePath": "Chemin à ignorer pour le disque", "displayCpuIndex": "Afficher l'index CPU", "dl2Local": "Télécharger {fileName} localement ?", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index b2d5f24d..c05b7732 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -36,6 +36,7 @@ "dirEmpty": "Pastikan dir kosong.", "disconnected": "Terputus", "disk": "Disk", + "diskHealth": "Kesehatan disk", "diskIgnorePath": "Abaikan jalan untuk disk", "displayCpuIndex": "Tampilkan indeks CPU", "dl2Local": "Unduh {fileName} ke lokal?", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index eccade00..361e4626 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -36,6 +36,7 @@ "dirEmpty": "フォルダーが空であることを確認してください", "disconnected": "接続が切断されました", "disk": "ディスク", + "diskHealth": "ディスクの健康状態", "diskIgnorePath": "無視されたディスクパス", "displayCpuIndex": "CPUインデックスを表示する", "dl2Local": "{fileName}をローカルにダウンロードしますか?", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 1f6b8ac9..21842659 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -36,6 +36,7 @@ "dirEmpty": "Zorg ervoor dat de map leeg is.", "disconnected": "Verbroken", "disk": "Schijf", + "diskHealth": "Schijfgezondheid", "diskIgnorePath": "Pad negeren voor schijf", "displayCpuIndex": "Toon de CPU-index", "dl2Local": "Download {fileName} naar lokaal?", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index aef304ac..177fe524 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -36,6 +36,7 @@ "dirEmpty": "Certifique-se de que a pasta está vazia", "disconnected": "Desconectado", "disk": "Disco", + "diskHealth": "Saúde do disco", "diskIgnorePath": "Caminhos de disco ignorados", "displayCpuIndex": "Exiba o índice de CPU", "dl2Local": "Baixar {fileName} para o local?", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 886a197e..a3cfef3e 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -36,6 +36,7 @@ "dirEmpty": "Пожалуйста, убедитесь, что папка пуста", "disconnected": "Отключено", "disk": "Диск", + "diskHealth": "Состояние диска", "diskIgnorePath": "Игнорировать путь к диску", "displayCpuIndex": "Отобразить индекс ЦП", "dl2Local": "Загрузить {fileName} на локальный диск?", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 469556b2..b25f243c 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -36,6 +36,7 @@ "dirEmpty": "Klasörün boş olduğundan emin olun.", "disconnected": "Bağlantı kesildi", "disk": "Disk", + "diskHealth": "Disk sağlığı", "diskIgnorePath": "Disk için yok sayılan yol", "displayCpuIndex": "CPU indeksini göster", "dl2Local": "{fileName} dosyasını yerel cihaza indir?", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3a9da383..3bcf4d12 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -36,6 +36,7 @@ "dirEmpty": "Переконайтеся, що директорія пуста.", "disconnected": "Відключено", "disk": "Диск", + "diskHealth": "Стан диска", "diskIgnorePath": "Ігнорувати шлях для диска", "displayCpuIndex": "Відобразити індекс ЦП", "dl2Local": "Завантажити {fileName} на локальний комп'ютер?", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1bcf220b..ca54342f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -36,6 +36,7 @@ "dirEmpty": "请确保文件夹为空", "disconnected": "连接断开", "disk": "磁盘", + "diskHealth": "磁盘健康", "diskIgnorePath": "忽略的磁盘路径", "displayCpuIndex": "显示 CPU 索引", "dl2Local": "下载 {fileName} 到本地?", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 775b0f3d..f6bf86d0 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -36,6 +36,7 @@ "dirEmpty": "請確保資料夾為空", "disconnected": "連接斷開", "disk": "磁碟", + "diskHealth": "磁碟健康", "diskIgnorePath": "忽略的磁碟路徑", "displayCpuIndex": "顯示 CPU 索引", "dl2Local": "下載 {fileName} 到本地?", diff --git a/lib/view/page/server/detail/view.dart b/lib/view/page/server/detail/view.dart index ffc75a56..3b17ba1a 100644 --- a/lib/view/page/server/detail/view.dart +++ b/lib/view/page/server/detail/view.dart @@ -11,6 +11,7 @@ import 'package:server_box/data/model/app/shell_func.dart'; import 'package:server_box/data/model/server/battery.dart'; import 'package:server_box/data/model/server/cpu.dart'; import 'package:server_box/data/model/server/disk.dart'; +import 'package:server_box/data/model/server/disk_smart.dart'; import 'package:server_box/data/model/server/dist.dart'; import 'package:server_box/data/model/server/net_speed.dart'; import 'package:server_box/data/model/server/nvdia.dart'; @@ -43,6 +44,7 @@ class _ServerDetailPageState extends State with SingleTickerPr _buildSwapView, _buildGpuView, _buildDiskView, + _buildDiskSmart, _buildNetView, _buildSensors, _buildTemperature, @@ -147,7 +149,7 @@ class _ServerDetailPageState extends State with SingleTickerPr return ExtendedImage.network( logoUrl, cache: true, - height: cons.maxHeight * 0.2, + height: cons.maxWidth * 0.3, width: cons.maxWidth, ); }, @@ -563,6 +565,55 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } + Widget? _buildDiskSmart(Server si) { + final smarts = si.status.diskSmart; + if (smarts.isEmpty) return null; + return CardX( + child: ExpandTile( + title: Text(l10n.diskHealth), + leading: Icon(ServerDetailCards.smart.icon, size: 17), + childrenPadding: const EdgeInsets.only(bottom: 7), + initiallyExpanded: _getInitExpand(smarts.length), + children: smarts.map(_buildDiskSmartItem).toList(), + ), + ); + } + + Widget _buildDiskSmartItem(DiskSmart smart) { + final isPass = smart.healthy ?? false; + final statusText = isPass ? 'PASS' : 'FAIL'; + final statusColor = isPass ? Colors.green : Colors.red; + final statusIcon = isPass + ? Icon(Icons.check_circle, color: Colors.green, size: 18) + : Icon(Icons.error, color: Colors.red, size: 18); + + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + leading: statusIcon, + title: Text(smart.device, style: UIs.text13, textScaler: _textFactor), + trailing: Text( + statusText, + style: UIs.text13.copyWith(color: statusColor, fontWeight: FontWeight.bold), + textScaler: _textFactor, + ), + subtitle: _buildDiskSmartDetails(smart), + ); + } + + Widget? _buildDiskSmartDetails(DiskSmart smart) { + final details = []; + + if (smart.model != null) details.add(smart.model!); + if (smart.serial != null) details.add('S/N: ${smart.serial}'); + if (smart.temperature != null) details.add('${smart.temperature!.toStringAsFixed(1)}°C'); + if (smart.powerOnHours != null) details.add('${smart.powerOnHours} hours'); + + if (details.isEmpty) return null; + + return Text(details.join(' | '), style: UIs.text12, textScaler: _textFactor); + } + Widget? _buildNetView(Server si) { final ss = si.status; final ns = ss.netSpeed; diff --git a/pubspec.lock b/pubspec.lock index dc1c4b0c..83dbfde6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -258,6 +258,14 @@ packages: url: "https://github.com/lollipopkit/circle_chart" source: git version: "0.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -299,6 +307,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "4b8701e48a58f7712492c9b1f7ba0bb9d525644dd66d023b62e1fc8cdb560c8a" + url: "https://pub.dev" + source: hosted + version: "1.14.0" cross_file: dependency: transitive description: @@ -939,6 +955,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" package_config: dependency: transitive description: @@ -1340,6 +1364,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1369,6 +1409,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.5" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -1433,6 +1489,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + url: "https://pub.dev" + source: hosted + version: "1.25.15" test_api: dependency: transitive description: @@ -1441,6 +1505,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + test_core: + dependency: transitive + description: + name: test_core + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + url: "https://pub.dev" + source: hosted + version: "0.6.8" timing: dependency: transitive description: @@ -1658,6 +1730,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8ff3ffc5..e90e4e3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dev_dependencies: json_serializable: ^6.8.0 freezed: ^2.5.7 riverpod_generator: ^2.6.3 + test: ^1.24.0 flutter_test: sdk: flutter fl_build: diff --git a/test/disk_smart_test.dart b/test/disk_smart_test.dart new file mode 100644 index 00000000..eca570d2 --- /dev/null +++ b/test/disk_smart_test.dart @@ -0,0 +1,550 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:server_box/data/model/server/disk_smart.dart'; + +const _raw = ''' +{ + "json_format_version": [ + 1, + 0 + ], + "smartctl": { + "version": [ + 7, + 4 + ], + "pre_release": false, + "svn_revision": "5530", + "platform_info": "x86_64-linux-6.6.58-rt45-intel-ese-standard-lts-rt", + "build_info": "(local build)", + "argv": [ + "smartctl", + "-A", + "-j", + "/dev/sda" + ], + "drive_database_version": { + "string": "7.3/5528" + }, + "exit_status": 0 + }, + "local_time": { + "time_t": 1749074092, + "asctime": "Thu Jun 5 05:54:52 2025 CST" + }, + "device": { + "name": "/dev/sda", + "info_name": "/dev/sda [SAT]", + "type": "sat", + "protocol": "ATA" + }, + "ata_smart_attributes": { + "revision": 16, + "table": [ + { + "id": 9, + "name": "Power_On_Hours", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 17472, + "string": "17472" + } + }, + { + "id": 12, + "name": "Power_Cycle_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 1948, + "string": "1948" + } + }, + { + "id": 167, + "name": "Write_Protect_Mode", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 34, + "string": "-O---K ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 168, + "name": "SATA_Phy_Error_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 172, + "name": "Erase_Fail_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 173, + "name": "MaxAvgErase_Ct", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 8257696, + "string": "160 (Average 126)" + } + }, + { + "id": 181, + "name": "Program_Fail_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 187, + "name": "Reported_Uncorrect", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 50, + "string": "-O--CK ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": true + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 192, + "name": "Unsafe_Shutdown_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 141, + "string": "141" + } + }, + { + "id": 194, + "name": "Temperature_Celsius", + "value": 65, + "worst": 39, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 35, + "string": "PO---K ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": true + }, + "raw": { + "value": 261993922595, + "string": "35 (Min/Max 14/61)" + } + }, + { + "id": 196, + "name": "Reallocated_Event_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 218, + "name": "CRC_Error_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 0, + "string": "0" + } + }, + { + "id": 231, + "name": "SSD_Life_Left", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 19, + "string": "PO--C- ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 93, + "string": "93" + } + }, + { + "id": 233, + "name": "Flash_Writes_GiB", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 19, + "string": "PO--C- ", + "prefailure": true, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 17618, + "string": "17618" + } + }, + { + "id": 241, + "name": "Lifetime_Writes_GiB", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 11520, + "string": "11520" + } + }, + { + "id": 242, + "name": "Lifetime_Reads_GiB", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 18, + "string": "-O--C- ", + "prefailure": false, + "updated_online": true, + "performance": false, + "error_rate": false, + "event_count": true, + "auto_keep": false + }, + "raw": { + "value": 12361, + "string": "12361" + } + }, + { + "id": 244, + "name": "Average_Erase_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 126, + "string": "126" + } + }, + { + "id": 245, + "name": "Max_Erase_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 160, + "string": "160" + } + }, + { + "id": 246, + "name": "Total_Erase_Count", + "value": 100, + "worst": 100, + "thresh": 0, + "when_failed": "", + "flags": { + "value": 0, + "string": "------ ", + "prefailure": false, + "updated_online": false, + "performance": false, + "error_rate": false, + "event_count": false, + "auto_keep": false + }, + "raw": { + "value": 2749648, + "string": "2749648" + } + } + ] + }, + "power_on_time": { + "hours": 17472 + }, + "power_cycle_count": 1948, + "temperature": { + "current": 35 + } +}'''; + +void main() { + group('DiskSmart', () { + late DiskSmart diskSmart; + + setUp(() { + final parsedResults = DiskSmart.parse(_raw); + expect(parsedResults.length, 1, reason: 'Should parse one disk entry'); + diskSmart = parsedResults.first; + }); + + test('parses basic device info correctly', () { + expect(diskSmart.device, '/dev/sda'); + expect(diskSmart.temperature, 35); + expect(diskSmart.powerOnHours, 17472); + expect(diskSmart.powerCycleCount, 1948); + }); + + test('has correct SMART attributes', () { + expect(diskSmart.smartAttributes.length, isNot(0)); + final tempAttr = diskSmart.getAttribute('Temperature_Celsius'); + expect(tempAttr, isNotNull); + expect(tempAttr?.value, 65); + expect(tempAttr?.worst, 39); + expect(tempAttr?.rawString, '35 (Min/Max 14/61)'); + + final powerOnAttr = diskSmart.getAttribute('Power_On_Hours'); + expect(powerOnAttr?.rawValue, 17472); + + // Test non-existent attribute + expect(diskSmart.getAttribute('NonExistent'), isNull); + }); + + test('extracts attribute flags correctly', () { + final tempAttr = diskSmart.getAttribute('Temperature_Celsius'); + expect(tempAttr?.flags.prefailure, isTrue); + expect(tempAttr?.flags.updatedOnline, isTrue); + expect(tempAttr?.flags.performance, isFalse); + + final lifeLeftAttr = diskSmart.getAttribute('SSD_Life_Left'); + expect(lifeLeftAttr?.flags.prefailure, isTrue); + expect(lifeLeftAttr?.flags.eventCount, isTrue); + }); + + test('calculates SSD health metrics correctly', () { + expect(diskSmart.ssdLifeLeft, 93); + expect(diskSmart.lifetimeWritesGiB, 11520); + expect(diskSmart.lifetimeReadsGiB, 12361); + expect(diskSmart.unsafeShutdownCount, 141); + expect(diskSmart.averageEraseCount, 126); + expect(diskSmart.maxEraseCount, 160); + }); + + test('toMap() converts all important data', () { + final map = diskSmart.toJson(); + expect(map['device'], '/dev/sda'); + expect(map['temperature'], 35); + expect(map['powerOnHours'], 17472); + expect(map['powerCycleCount'], 1948); + expect(map['smartAttributes'], isA()); + }); + }); + + group('DiskSmart parsing edge cases', () { + test('handles empty input', () { + final results = DiskSmart.parse(''); + expect(results, isEmpty); + }); + + test('handles malformed JSON gracefully', () { + final results = DiskSmart.parse('{not valid json}'); + expect(results, isEmpty); + }); + + test('handles multiple disk data', () { + final results = DiskSmart.parse('$_raw\n\n$_raw'); + expect(results.length, 2); + }); + }); +}