mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat: disk smart info (#773)
This commit is contained in:
@@ -11,13 +11,13 @@ enum ServerDetailCards {
|
|||||||
swap(Icons.swap_horiz),
|
swap(Icons.swap_horiz),
|
||||||
gpu(Bootstrap.gpu_card),
|
gpu(Bootstrap.gpu_card),
|
||||||
disk(Bootstrap.device_hdd_fill),
|
disk(Bootstrap.device_hdd_fill),
|
||||||
|
smart(Icons.health_and_safety, sinceBuild: 1174),
|
||||||
net(ZondIcons.network),
|
net(ZondIcons.network),
|
||||||
sensor(MingCute.dashboard_4_line),
|
sensor(MingCute.dashboard_4_line),
|
||||||
temp(FontAwesome.temperature_empty_solid),
|
temp(FontAwesome.temperature_empty_solid),
|
||||||
battery(Icons.battery_full),
|
battery(Icons.battery_full),
|
||||||
pve(BoxIcons.bxs_dashboard, sinceBuild: 818),
|
pve(BoxIcons.bxs_dashboard, sinceBuild: 818),
|
||||||
custom(Icons.code, sinceBuild: 825),
|
custom(Icons.code, sinceBuild: 825);
|
||||||
;
|
|
||||||
|
|
||||||
final int? sinceBuild;
|
final int? sinceBuild;
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ enum ServerDetailCards {
|
|||||||
swap => 'Swap',
|
swap => 'Swap',
|
||||||
gpu => 'GPU',
|
gpu => 'GPU',
|
||||||
disk => l10n.disk,
|
disk => l10n.disk,
|
||||||
|
smart => l10n.diskHealth,
|
||||||
net => l10n.net,
|
net => l10n.net,
|
||||||
sensor => l10n.sensors,
|
sensor => l10n.sensors,
|
||||||
temp => l10n.temperature,
|
temp => l10n.temperature,
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ enum ShellFunc {
|
|||||||
process,
|
process,
|
||||||
shutdown,
|
shutdown,
|
||||||
reboot,
|
reboot,
|
||||||
suspend,
|
suspend;
|
||||||
;
|
|
||||||
|
|
||||||
static const seperator = 'SrvBoxSep';
|
static const seperator = 'SrvBoxSep';
|
||||||
|
|
||||||
@@ -29,7 +28,9 @@ enum ShellFunc {
|
|||||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
/// it will be changed to [scriptDirHome]/[scriptFile].
|
||||||
static String getScriptDir(String id) {
|
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;
|
if (customScriptDir != null) return customScriptDir;
|
||||||
return _scriptDirMap.putIfAbsent(id, () {
|
return _scriptDirMap.putIfAbsent(id, () {
|
||||||
return scriptDirTmp;
|
return scriptDirTmp;
|
||||||
@@ -162,7 +163,9 @@ exec 2>/dev/null
|
|||||||
// Write each func
|
// Write each func
|
||||||
for (final func in values) {
|
for (final func in values) {
|
||||||
final customCmdsStr = () {
|
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 '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
@@ -209,17 +212,21 @@ enum StatusCmdType {
|
|||||||
cpu._('cat /proc/stat | grep cpu'),
|
cpu._('cat /proc/stat | grep cpu'),
|
||||||
uptime._('uptime'),
|
uptime._('uptime'),
|
||||||
conn._('cat /proc/net/snmp'),
|
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'"),
|
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||||
host._('cat /etc/hostname'),
|
host._('cat /etc/hostname'),
|
||||||
diskio._('cat /proc/diskstats'),
|
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'),
|
nvidia._('nvidia-smi -q -x'),
|
||||||
sensors._('sensors'),
|
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;
|
final String cmd;
|
||||||
|
|
||||||
@@ -238,8 +245,7 @@ enum BSDStatusCmdType {
|
|||||||
mem._('top -l 1 | grep PhysMem'),
|
mem._('top -l 1 | grep PhysMem'),
|
||||||
//temp,
|
//temp,
|
||||||
host._('hostname'),
|
host._('hostname'),
|
||||||
cpuBrand._('sysctl -n machdep.cpu.brand_string'),
|
cpuBrand._('sysctl -n machdep.cpu.brand_string');
|
||||||
;
|
|
||||||
|
|
||||||
final String cmd;
|
final String cmd;
|
||||||
|
|
||||||
|
|||||||
204
lib/data/model/server/disk_smart.dart
Normal file
204
lib/data/model/server/disk_smart.dart
Normal file
@@ -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<String, dynamic> rawData,
|
||||||
|
required Map<String, SmartAttribute> smartAttributes,
|
||||||
|
}) = _DiskSmart;
|
||||||
|
|
||||||
|
factory DiskSmart.fromJson(Map<String, dynamic> json) => _$DiskSmartFromJson(json);
|
||||||
|
|
||||||
|
static List<DiskSmart> parse(String raw) {
|
||||||
|
final results = <DiskSmart>[];
|
||||||
|
|
||||||
|
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<String, dynamic>;
|
||||||
|
|
||||||
|
// 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<String, SmartAttribute> _parseSmartAttributes(Map<String, dynamic> data) {
|
||||||
|
final attributes = <String, SmartAttribute>{};
|
||||||
|
|
||||||
|
final attrTable = data['ata_smart_attributes']?['table'] as List?;
|
||||||
|
if (attrTable == null) return attributes;
|
||||||
|
|
||||||
|
for (final attr in attrTable) {
|
||||||
|
if (attr is Map<String, dynamic>) {
|
||||||
|
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<String, dynamic>? ?? {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final _tempReg = RegExp(r'^(\d+(?:\.\d+)?)');
|
||||||
|
|
||||||
|
/// Extract temperature from the data
|
||||||
|
static double? _extractTemperature(Map<String, dynamic> data, Map<String, SmartAttribute> 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<String, dynamic> 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<String, dynamic> json) => _$SmartAttributeFlagsFromJson(json);
|
||||||
|
|
||||||
|
factory SmartAttributeFlags.fromMap(Map<String, dynamic> 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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
1038
lib/data/model/server/disk_smart.freezed.dart
Normal file
1038
lib/data/model/server/disk_smart.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
91
lib/data/model/server/disk_smart.g.dart
Normal file
91
lib/data/model/server/disk_smart.g.dart
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'disk_smart.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$DiskSmartImpl _$$DiskSmartImplFromJson(Map<String, dynamic> 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<String, dynamic>,
|
||||||
|
smartAttributes: (json['smartAttributes'] as Map<String, dynamic>).map(
|
||||||
|
(k, e) =>
|
||||||
|
MapEntry(k, SmartAttribute.fromJson(e as Map<String, dynamic>)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$DiskSmartImplToJson(_$DiskSmartImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'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<String, dynamic> 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<String, dynamic>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SmartAttributeImplToJson(
|
||||||
|
_$SmartAttributeImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'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<String, dynamic> 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<String, dynamic> _$$SmartAttributeFlagsImplToJson(
|
||||||
|
_$SmartAttributeFlagsImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'value': instance.value,
|
||||||
|
'string': instance.string,
|
||||||
|
'prefailure': instance.prefailure,
|
||||||
|
'updatedOnline': instance.updatedOnline,
|
||||||
|
'performance': instance.performance,
|
||||||
|
'errorRate': instance.errorRate,
|
||||||
|
'eventCount': instance.eventCount,
|
||||||
|
'autoKeep': instance.autoKeep,
|
||||||
|
};
|
||||||
@@ -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/conn.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.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.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/memory.dart';
|
||||||
import 'package:server_box/data/model/server/net_speed.dart';
|
import 'package:server_box/data/model/server/net_speed.dart';
|
||||||
import 'package:server_box/data/model/server/nvdia.dart';
|
import 'package:server_box/data/model/server/nvdia.dart';
|
||||||
@@ -19,12 +20,7 @@ class Server {
|
|||||||
SSHClient? client;
|
SSHClient? client;
|
||||||
ServerConn conn;
|
ServerConn conn;
|
||||||
|
|
||||||
Server(
|
Server(this.spi, this.status, this.conn, {this.client});
|
||||||
this.spi,
|
|
||||||
this.status,
|
|
||||||
this.conn, {
|
|
||||||
this.client,
|
|
||||||
});
|
|
||||||
|
|
||||||
bool get needGenClient => conn < ServerConn.connecting;
|
bool get needGenClient => conn < ServerConn.connecting;
|
||||||
|
|
||||||
@@ -44,6 +40,7 @@ class ServerStatus {
|
|||||||
SystemType system;
|
SystemType system;
|
||||||
Err? err;
|
Err? err;
|
||||||
DiskIO diskIO;
|
DiskIO diskIO;
|
||||||
|
List<DiskSmart> diskSmart;
|
||||||
List<NvidiaSmiItem>? nvidia;
|
List<NvidiaSmiItem>? nvidia;
|
||||||
final List<Battery> batteries = [];
|
final List<Battery> batteries = [];
|
||||||
final Map<StatusCmdType, String> more = {};
|
final Map<StatusCmdType, String> more = {};
|
||||||
@@ -61,6 +58,7 @@ class ServerStatus {
|
|||||||
required this.temps,
|
required this.temps,
|
||||||
required this.system,
|
required this.system,
|
||||||
required this.diskIO,
|
required this.diskIO,
|
||||||
|
this.diskSmart = const [],
|
||||||
this.err,
|
this.err,
|
||||||
this.nvidia,
|
this.nvidia,
|
||||||
this.diskUsage,
|
this.diskUsage,
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ extension Spix on Spi {
|
|||||||
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
|
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
|
||||||
|
|
||||||
bool shouldReconnect(Spi old) {
|
bool shouldReconnect(Spi old) {
|
||||||
return id != old.id ||
|
return user != old.user ||
|
||||||
|
ip != old.ip ||
|
||||||
|
port != old.port ||
|
||||||
pwd != old.pwd ||
|
pwd != old.pwd ||
|
||||||
keyId != old.keyId ||
|
keyId != old.keyId ||
|
||||||
alterUrl != old.alterUrl ||
|
alterUrl != old.alterUrl ||
|
||||||
|
|||||||
@@ -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/conn.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.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.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/memory.dart';
|
||||||
import 'package:server_box/data/model/server/net_speed.dart';
|
import 'package:server_box/data/model/server/net_speed.dart';
|
||||||
import 'package:server_box/data/model/server/nvdia.dart';
|
import 'package:server_box/data/model/server/nvdia.dart';
|
||||||
@@ -37,7 +38,8 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
|||||||
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||||
final segments = req.segments;
|
final segments = req.segments;
|
||||||
|
|
||||||
final time = int.tryParse(StatusCmdType.time.find(segments)) ??
|
final time =
|
||||||
|
int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -48,9 +50,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sys = _parseSysVer(
|
final sys = _parseSysVer(StatusCmdType.sys.find(segments));
|
||||||
StatusCmdType.sys.find(segments),
|
|
||||||
);
|
|
||||||
if (sys != null) {
|
if (sys != null) {
|
||||||
req.ss.more[StatusCmdType.sys] = sys;
|
req.ss.more[StatusCmdType.sys] = sys;
|
||||||
}
|
}
|
||||||
@@ -130,6 +130,13 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
|||||||
Loggers.app.warning(e, s);
|
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 {
|
try {
|
||||||
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
|
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
|||||||
@@ -45,21 +45,20 @@ class ServerProvider extends Provider {
|
|||||||
for (int idx = 0; idx < spis.length; idx++) {
|
for (int idx = 0; idx < spis.length; idx++) {
|
||||||
final spi = spis[idx];
|
final spi = spis[idx];
|
||||||
final originServer = oldServers[spi.id];
|
final originServer = oldServers[spi.id];
|
||||||
final newServer = genServer(spi);
|
|
||||||
|
|
||||||
/// #258
|
/// #258
|
||||||
/// If not [shouldReconnect], then keep the old state.
|
/// If not [shouldReconnect], then keep the old state.
|
||||||
if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) {
|
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();
|
final serverOrder_ = Stores.setting.serverOrder.fetch();
|
||||||
if (serverOrder_.isNotEmpty) {
|
if (serverOrder_.isNotEmpty) {
|
||||||
spis.reorder(
|
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
|
||||||
order: serverOrder_,
|
|
||||||
finder: (n, id) => n.id == id,
|
|
||||||
);
|
|
||||||
serverOrder.value.addAll(spis.map((e) => e.id));
|
serverOrder.value.addAll(spis.map((e) => e.id));
|
||||||
} else {
|
} else {
|
||||||
serverOrder.value.addAll(servers.keys);
|
serverOrder.value.addAll(servers.keys);
|
||||||
@@ -104,17 +103,15 @@ class ServerProvider extends Provider {
|
|||||||
|
|
||||||
/// if [spi] is specificed then only refresh this server
|
/// if [spi] is specificed then only refresh this server
|
||||||
/// [onlyFailed] only refresh failed servers
|
/// [onlyFailed] only refresh failed servers
|
||||||
static Future<void> refresh({
|
static Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
|
||||||
Spi? spi,
|
|
||||||
bool onlyFailed = false,
|
|
||||||
}) async {
|
|
||||||
if (spi != null) {
|
if (spi != null) {
|
||||||
_manualDisconnectedIds.remove(spi.id);
|
_manualDisconnectedIds.remove(spi.id);
|
||||||
await _getData(spi);
|
await _getData(spi);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.wait(servers.values.map((val) async {
|
await Future.wait(
|
||||||
|
servers.values.map((val) async {
|
||||||
final s = val.value;
|
final s = val.value;
|
||||||
if (onlyFailed) {
|
if (onlyFailed) {
|
||||||
if (s.conn != ServerConn.failed) return;
|
if (s.conn != ServerConn.failed) return;
|
||||||
@@ -128,7 +125,8 @@ class ServerProvider extends Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await _getData(s.spi);
|
return await _getData(s.spi);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> startAutoRefresh() async {
|
static Future<void> startAutoRefresh() async {
|
||||||
@@ -307,14 +305,11 @@ class ServerProvider extends Provider {
|
|||||||
_setServerState(s, ServerConn.connected);
|
_setServerState(s, ServerConn.connected);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final (_, writeScriptResult) = await sv.client!.exec(
|
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
||||||
(session) async {
|
|
||||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||||
session.stdin.add(scriptRaw);
|
session.stdin.add(scriptRaw);
|
||||||
session.stdin.close();
|
session.stdin.close();
|
||||||
},
|
}, entry: ShellFunc.getInstallShellCmd(spi.id));
|
||||||
entry: ShellFunc.getInstallShellCmd(spi.id),
|
|
||||||
);
|
|
||||||
if (writeScriptResult.isNotEmpty) {
|
if (writeScriptResult.isNotEmpty) {
|
||||||
ShellFunc.switchScriptDir(spi.id);
|
ShellFunc.switchScriptDir(spi.id);
|
||||||
throw writeScriptResult;
|
throw writeScriptResult;
|
||||||
@@ -366,10 +361,7 @@ class ServerProvider extends Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TryLimiter.inc(sid);
|
TryLimiter.inc(sid);
|
||||||
sv.status.err = SSHErr(
|
sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
|
||||||
type: SSHErrType.segements,
|
|
||||||
message: 'Seperate segments failed, raw:\n$raw',
|
|
||||||
);
|
|
||||||
_setServerState(s, ServerConn.failed);
|
_setServerState(s, ServerConn.failed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -408,17 +400,10 @@ class ServerProvider extends Provider {
|
|||||||
system: systemType,
|
system: systemType,
|
||||||
customCmds: spi.custom?.cmds ?? {},
|
customCmds: spi.custom?.cmds ?? {},
|
||||||
);
|
);
|
||||||
sv.status = await Computer.shared.start(
|
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');
|
||||||
getStatus,
|
|
||||||
req,
|
|
||||||
taskName: 'StatusUpdateReq<${sv.id}>',
|
|
||||||
);
|
|
||||||
} catch (e, trace) {
|
} catch (e, trace) {
|
||||||
TryLimiter.inc(sid);
|
TryLimiter.inc(sid);
|
||||||
sv.status.err = SSHErr(
|
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
|
||||||
type: SSHErrType.getStatus,
|
|
||||||
message: 'Parse failed: $e\n\n$raw',
|
|
||||||
);
|
|
||||||
_setServerState(s, ServerConn.failed);
|
_setServerState(s, ServerConn.failed);
|
||||||
Loggers.app.warning('Server status', e, trace);
|
Loggers.app.warning('Server status', e, trace);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ abstract final class GithubIds {
|
|||||||
'dccif',
|
'dccif',
|
||||||
'mikropsoft',
|
'mikropsoft',
|
||||||
'CakesTwix',
|
'CakesTwix',
|
||||||
|
'dsvf',
|
||||||
|
'fei1025',
|
||||||
|
'MasedMSD',
|
||||||
|
'GitGitro',
|
||||||
|
'Shin-suechtig',
|
||||||
};
|
};
|
||||||
|
|
||||||
static const participants = <GhId>{
|
static const participants = <GhId>{
|
||||||
@@ -99,6 +104,20 @@ abstract final class GithubIds {
|
|||||||
'88484396',
|
'88484396',
|
||||||
'honggeigei',
|
'honggeigei',
|
||||||
'likecreep',
|
'likecreep',
|
||||||
|
'axlrose',
|
||||||
|
'immortal521',
|
||||||
|
'PRO-2684',
|
||||||
|
'Xiaobao-Yang',
|
||||||
|
'Mrhs121',
|
||||||
|
'Fudiautobi',
|
||||||
|
'papaj-na-wrotkach',
|
||||||
|
'kid1412621',
|
||||||
|
'smanx',
|
||||||
|
'xuanyue1024',
|
||||||
|
'RuofengX',
|
||||||
|
'rhwong',
|
||||||
|
'AstroEngineeer',
|
||||||
|
'mochasweet',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,37 +8,17 @@ import 'package:server_box/data/model/server/system.dart';
|
|||||||
import 'package:server_box/data/model/server/temp.dart';
|
import 'package:server_box/data/model/server/temp.dart';
|
||||||
|
|
||||||
abstract final class InitStatus {
|
abstract final class InitStatus {
|
||||||
static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore(
|
static SingleCpuCore get _initOneTimeCpuStatus =>
|
||||||
'cpu',
|
SingleCpuCore('cpu', 0, 0, 0, 0, 0, 0, 0);
|
||||||
0,
|
static Cpus get cpus =>
|
||||||
0,
|
Cpus([_initOneTimeCpuStatus], [_initOneTimeCpuStatus]);
|
||||||
0,
|
static NetSpeedPart get _initNetSpeedPart =>
|
||||||
0,
|
NetSpeedPart('', BigInt.zero, BigInt.zero, 0);
|
||||||
0,
|
static NetSpeed get netSpeed =>
|
||||||
0,
|
NetSpeed([_initNetSpeedPart], [_initNetSpeedPart]);
|
||||||
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(
|
static ServerStatus get status => ServerStatus(
|
||||||
cpu: cpus,
|
cpu: cpus,
|
||||||
mem: const Memory(
|
mem: const Memory(total: 1, free: 1, avail: 1),
|
||||||
total: 1,
|
|
||||||
free: 1,
|
|
||||||
avail: 1,
|
|
||||||
),
|
|
||||||
disk: [
|
disk: [
|
||||||
Disk(
|
Disk(
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -47,17 +27,14 @@ abstract final class InitStatus {
|
|||||||
used: BigInt.zero,
|
used: BigInt.zero,
|
||||||
size: BigInt.one,
|
size: BigInt.one,
|
||||||
avail: BigInt.zero,
|
avail: BigInt.zero,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
|
tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
|
||||||
netSpeed: netSpeed,
|
netSpeed: netSpeed,
|
||||||
swap: const Swap(
|
swap: const Swap(total: 0, free: 0, cached: 0),
|
||||||
total: 0,
|
|
||||||
free: 0,
|
|
||||||
cached: 0,
|
|
||||||
),
|
|
||||||
system: SystemType.linux,
|
system: SystemType.linux,
|
||||||
temps: Temperatures(),
|
temps: Temperatures(),
|
||||||
diskIO: DiskIO([], []),
|
diskIO: DiskIO([], []),
|
||||||
|
diskSmart: const [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,6 +335,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Disk'**
|
/// **'Disk'**
|
||||||
String get 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.
|
/// No description provided for @diskIgnorePath.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Festplatte';
|
String get disk => 'Festplatte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Festplattengesundheit';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Pfad für Datenträger ignorieren';
|
String get diskIgnorePath => 'Pfad für Datenträger ignorieren';
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Disk';
|
String get disk => 'Disk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Disk Health';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Ignore path for disk';
|
String get diskIgnorePath => 'Ignore path for disk';
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Disco';
|
String get disk => 'Disco';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Salud del disco';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Rutas de disco ignoradas';
|
String get diskIgnorePath => 'Rutas de disco ignoradas';
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Disque';
|
String get disk => 'Disque';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Santé du disque';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Chemin à ignorer pour le disque';
|
String get diskIgnorePath => 'Chemin à ignorer pour le disque';
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Disk';
|
String get disk => 'Disk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Kesehatan disk';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Abaikan jalan untuk disk';
|
String get diskIgnorePath => 'Abaikan jalan untuk disk';
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'ディスク';
|
String get disk => 'ディスク';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'ディスクの健康状態';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => '無視されたディスクパス';
|
String get diskIgnorePath => '無視されたディスクパス';
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Schijf';
|
String get disk => 'Schijf';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Schijfgezondheid';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Pad negeren voor schijf';
|
String get diskIgnorePath => 'Pad negeren voor schijf';
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Disco';
|
String get disk => 'Disco';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Saúde do disco';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Caminhos de disco ignorados';
|
String get diskIgnorePath => 'Caminhos de disco ignorados';
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Диск';
|
String get disk => 'Диск';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Состояние диска';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Игнорировать путь к диску';
|
String get diskIgnorePath => 'Игнорировать путь к диску';
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Disk';
|
String get disk => 'Disk';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Disk sağlığı';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Disk için yok sayılan yol';
|
String get diskIgnorePath => 'Disk için yok sayılan yol';
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => 'Диск';
|
String get disk => 'Диск';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => 'Стан диска';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => 'Ігнорувати шлях для диска';
|
String get diskIgnorePath => 'Ігнорувати шлях для диска';
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disk => '磁盘';
|
String get disk => '磁盘';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => '磁盘健康';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => '忽略的磁盘路径';
|
String get diskIgnorePath => '忽略的磁盘路径';
|
||||||
|
|
||||||
@@ -844,6 +847,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get disk => '磁碟';
|
String get disk => '磁碟';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get diskHealth => '磁碟健康';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get diskIgnorePath => '忽略的磁碟路徑';
|
String get diskIgnorePath => '忽略的磁碟路徑';
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Stelle sicher, dass der Ordner leer ist.",
|
"dirEmpty": "Stelle sicher, dass der Ordner leer ist.",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Disconnected",
|
||||||
"disk": "Festplatte",
|
"disk": "Festplatte",
|
||||||
|
"diskHealth": "Festplattengesundheit",
|
||||||
"diskIgnorePath": "Pfad für Datenträger ignorieren",
|
"diskIgnorePath": "Pfad für Datenträger ignorieren",
|
||||||
"displayCpuIndex": "Zeigen Sie den CPU-Index an",
|
"displayCpuIndex": "Zeigen Sie den CPU-Index an",
|
||||||
"dl2Local": "Datei \"{fileName}\" herunterladen?",
|
"dl2Local": "Datei \"{fileName}\" herunterladen?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Make sure the folder is empty.",
|
"dirEmpty": "Make sure the folder is empty.",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Disconnected",
|
||||||
"disk": "Disk",
|
"disk": "Disk",
|
||||||
|
"diskHealth": "Disk Health",
|
||||||
"diskIgnorePath": "Ignore path for disk",
|
"diskIgnorePath": "Ignore path for disk",
|
||||||
"displayCpuIndex": "Display CPU index",
|
"displayCpuIndex": "Display CPU index",
|
||||||
"dl2Local": "Download {fileName} to local?",
|
"dl2Local": "Download {fileName} to local?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Asegúrate de que el directorio esté vacío",
|
"dirEmpty": "Asegúrate de que el directorio esté vacío",
|
||||||
"disconnected": "Desconectado",
|
"disconnected": "Desconectado",
|
||||||
"disk": "Disco",
|
"disk": "Disco",
|
||||||
|
"diskHealth": "Salud del disco",
|
||||||
"diskIgnorePath": "Rutas de disco ignoradas",
|
"diskIgnorePath": "Rutas de disco ignoradas",
|
||||||
"displayCpuIndex": "Muestre el índice de CPU",
|
"displayCpuIndex": "Muestre el índice de CPU",
|
||||||
"dl2Local": "¿Descargar {fileName} a local?",
|
"dl2Local": "¿Descargar {fileName} a local?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Assurez-vous que le répertoire est vide.",
|
"dirEmpty": "Assurez-vous que le répertoire est vide.",
|
||||||
"disconnected": "Déconnecté",
|
"disconnected": "Déconnecté",
|
||||||
"disk": "Disque",
|
"disk": "Disque",
|
||||||
|
"diskHealth": "Santé du disque",
|
||||||
"diskIgnorePath": "Chemin à ignorer pour le disque",
|
"diskIgnorePath": "Chemin à ignorer pour le disque",
|
||||||
"displayCpuIndex": "Afficher l'index CPU",
|
"displayCpuIndex": "Afficher l'index CPU",
|
||||||
"dl2Local": "Télécharger {fileName} localement ?",
|
"dl2Local": "Télécharger {fileName} localement ?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Pastikan dir kosong.",
|
"dirEmpty": "Pastikan dir kosong.",
|
||||||
"disconnected": "Terputus",
|
"disconnected": "Terputus",
|
||||||
"disk": "Disk",
|
"disk": "Disk",
|
||||||
|
"diskHealth": "Kesehatan disk",
|
||||||
"diskIgnorePath": "Abaikan jalan untuk disk",
|
"diskIgnorePath": "Abaikan jalan untuk disk",
|
||||||
"displayCpuIndex": "Tampilkan indeks CPU",
|
"displayCpuIndex": "Tampilkan indeks CPU",
|
||||||
"dl2Local": "Unduh {fileName} ke lokal?",
|
"dl2Local": "Unduh {fileName} ke lokal?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "フォルダーが空であることを確認してください",
|
"dirEmpty": "フォルダーが空であることを確認してください",
|
||||||
"disconnected": "接続が切断されました",
|
"disconnected": "接続が切断されました",
|
||||||
"disk": "ディスク",
|
"disk": "ディスク",
|
||||||
|
"diskHealth": "ディスクの健康状態",
|
||||||
"diskIgnorePath": "無視されたディスクパス",
|
"diskIgnorePath": "無視されたディスクパス",
|
||||||
"displayCpuIndex": "CPUインデックスを表示する",
|
"displayCpuIndex": "CPUインデックスを表示する",
|
||||||
"dl2Local": "{fileName}をローカルにダウンロードしますか?",
|
"dl2Local": "{fileName}をローカルにダウンロードしますか?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Zorg ervoor dat de map leeg is.",
|
"dirEmpty": "Zorg ervoor dat de map leeg is.",
|
||||||
"disconnected": "Verbroken",
|
"disconnected": "Verbroken",
|
||||||
"disk": "Schijf",
|
"disk": "Schijf",
|
||||||
|
"diskHealth": "Schijfgezondheid",
|
||||||
"diskIgnorePath": "Pad negeren voor schijf",
|
"diskIgnorePath": "Pad negeren voor schijf",
|
||||||
"displayCpuIndex": "Toon de CPU-index",
|
"displayCpuIndex": "Toon de CPU-index",
|
||||||
"dl2Local": "Download {fileName} naar lokaal?",
|
"dl2Local": "Download {fileName} naar lokaal?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Certifique-se de que a pasta está vazia",
|
"dirEmpty": "Certifique-se de que a pasta está vazia",
|
||||||
"disconnected": "Desconectado",
|
"disconnected": "Desconectado",
|
||||||
"disk": "Disco",
|
"disk": "Disco",
|
||||||
|
"diskHealth": "Saúde do disco",
|
||||||
"diskIgnorePath": "Caminhos de disco ignorados",
|
"diskIgnorePath": "Caminhos de disco ignorados",
|
||||||
"displayCpuIndex": "Exiba o índice de CPU",
|
"displayCpuIndex": "Exiba o índice de CPU",
|
||||||
"dl2Local": "Baixar {fileName} para o local?",
|
"dl2Local": "Baixar {fileName} para o local?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
|
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
|
||||||
"disconnected": "Отключено",
|
"disconnected": "Отключено",
|
||||||
"disk": "Диск",
|
"disk": "Диск",
|
||||||
|
"diskHealth": "Состояние диска",
|
||||||
"diskIgnorePath": "Игнорировать путь к диску",
|
"diskIgnorePath": "Игнорировать путь к диску",
|
||||||
"displayCpuIndex": "Отобразить индекс ЦП",
|
"displayCpuIndex": "Отобразить индекс ЦП",
|
||||||
"dl2Local": "Загрузить {fileName} на локальный диск?",
|
"dl2Local": "Загрузить {fileName} на локальный диск?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Klasörün boş olduğundan emin olun.",
|
"dirEmpty": "Klasörün boş olduğundan emin olun.",
|
||||||
"disconnected": "Bağlantı kesildi",
|
"disconnected": "Bağlantı kesildi",
|
||||||
"disk": "Disk",
|
"disk": "Disk",
|
||||||
|
"diskHealth": "Disk sağlığı",
|
||||||
"diskIgnorePath": "Disk için yok sayılan yol",
|
"diskIgnorePath": "Disk için yok sayılan yol",
|
||||||
"displayCpuIndex": "CPU indeksini göster",
|
"displayCpuIndex": "CPU indeksini göster",
|
||||||
"dl2Local": "{fileName} dosyasını yerel cihaza indir?",
|
"dl2Local": "{fileName} dosyasını yerel cihaza indir?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "Переконайтеся, що директорія пуста.",
|
"dirEmpty": "Переконайтеся, що директорія пуста.",
|
||||||
"disconnected": "Відключено",
|
"disconnected": "Відключено",
|
||||||
"disk": "Диск",
|
"disk": "Диск",
|
||||||
|
"diskHealth": "Стан диска",
|
||||||
"diskIgnorePath": "Ігнорувати шлях для диска",
|
"diskIgnorePath": "Ігнорувати шлях для диска",
|
||||||
"displayCpuIndex": "Відобразити індекс ЦП",
|
"displayCpuIndex": "Відобразити індекс ЦП",
|
||||||
"dl2Local": "Завантажити {fileName} на локальний комп'ютер?",
|
"dl2Local": "Завантажити {fileName} на локальний комп'ютер?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "请确保文件夹为空",
|
"dirEmpty": "请确保文件夹为空",
|
||||||
"disconnected": "连接断开",
|
"disconnected": "连接断开",
|
||||||
"disk": "磁盘",
|
"disk": "磁盘",
|
||||||
|
"diskHealth": "磁盘健康",
|
||||||
"diskIgnorePath": "忽略的磁盘路径",
|
"diskIgnorePath": "忽略的磁盘路径",
|
||||||
"displayCpuIndex": "显示 CPU 索引",
|
"displayCpuIndex": "显示 CPU 索引",
|
||||||
"dl2Local": "下载 {fileName} 到本地?",
|
"dl2Local": "下载 {fileName} 到本地?",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dirEmpty": "請確保資料夾為空",
|
"dirEmpty": "請確保資料夾為空",
|
||||||
"disconnected": "連接斷開",
|
"disconnected": "連接斷開",
|
||||||
"disk": "磁碟",
|
"disk": "磁碟",
|
||||||
|
"diskHealth": "磁碟健康",
|
||||||
"diskIgnorePath": "忽略的磁碟路徑",
|
"diskIgnorePath": "忽略的磁碟路徑",
|
||||||
"displayCpuIndex": "顯示 CPU 索引",
|
"displayCpuIndex": "顯示 CPU 索引",
|
||||||
"dl2Local": "下載 {fileName} 到本地?",
|
"dl2Local": "下載 {fileName} 到本地?",
|
||||||
|
|||||||
@@ -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/battery.dart';
|
||||||
import 'package:server_box/data/model/server/cpu.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.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/dist.dart';
|
||||||
import 'package:server_box/data/model/server/net_speed.dart';
|
import 'package:server_box/data/model/server/net_speed.dart';
|
||||||
import 'package:server_box/data/model/server/nvdia.dart';
|
import 'package:server_box/data/model/server/nvdia.dart';
|
||||||
@@ -43,6 +44,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
_buildSwapView,
|
_buildSwapView,
|
||||||
_buildGpuView,
|
_buildGpuView,
|
||||||
_buildDiskView,
|
_buildDiskView,
|
||||||
|
_buildDiskSmart,
|
||||||
_buildNetView,
|
_buildNetView,
|
||||||
_buildSensors,
|
_buildSensors,
|
||||||
_buildTemperature,
|
_buildTemperature,
|
||||||
@@ -147,7 +149,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
|||||||
return ExtendedImage.network(
|
return ExtendedImage.network(
|
||||||
logoUrl,
|
logoUrl,
|
||||||
cache: true,
|
cache: true,
|
||||||
height: cons.maxHeight * 0.2,
|
height: cons.maxWidth * 0.3,
|
||||||
width: cons.maxWidth,
|
width: cons.maxWidth,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -563,6 +565,55 @@ class _ServerDetailPageState extends State<ServerDetailPage> 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 = <String>[];
|
||||||
|
|
||||||
|
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) {
|
Widget? _buildNetView(Server si) {
|
||||||
final ss = si.status;
|
final ss = si.status;
|
||||||
final ns = ss.netSpeed;
|
final ns = ss.netSpeed;
|
||||||
|
|||||||
80
pubspec.lock
80
pubspec.lock
@@ -258,6 +258,14 @@ packages:
|
|||||||
url: "https://github.com/lollipopkit/circle_chart"
|
url: "https://github.com/lollipopkit/circle_chart"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.3"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -299,6 +307,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
coverage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: coverage
|
||||||
|
sha256: "4b8701e48a58f7712492c9b1f7ba0bb9d525644dd66d023b62e1fc8cdb560c8a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.14.0"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -939,6 +955,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1340,6 +1364,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.2"
|
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:
|
shelf_web_socket:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1369,6 +1409,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.5"
|
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:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1433,6 +1489,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
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:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1441,6 +1505,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
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:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1658,6 +1730,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
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:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ dev_dependencies:
|
|||||||
json_serializable: ^6.8.0
|
json_serializable: ^6.8.0
|
||||||
freezed: ^2.5.7
|
freezed: ^2.5.7
|
||||||
riverpod_generator: ^2.6.3
|
riverpod_generator: ^2.6.3
|
||||||
|
test: ^1.24.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
fl_build:
|
fl_build:
|
||||||
|
|||||||
550
test/disk_smart_test.dart
Normal file
550
test/disk_smart_test.dart
Normal file
@@ -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<Map>());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user