feat: disk smart info (#773)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-06-05 07:31:45 +08:00
committed by GitHub
parent 741a6442e0
commit 176cb7da03
41 changed files with 2225 additions and 157 deletions

View File

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

View File

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

View 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)';
}
}

File diff suppressed because it is too large Load Diff

View 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,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 => '無視されたディスクパス';

View File

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

View File

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

View File

@@ -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 => 'Игнорировать путь к диску';

View File

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

View File

@@ -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 => 'Ігнорувати шлях для диска';

View File

@@ -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 => '忽略的磁碟路徑';

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -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 ?",

View File

@@ -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?",

View File

@@ -36,6 +36,7 @@
"dirEmpty": "フォルダーが空であることを確認してください", "dirEmpty": "フォルダーが空であることを確認してください",
"disconnected": "接続が切断されました", "disconnected": "接続が切断されました",
"disk": "ディスク", "disk": "ディスク",
"diskHealth": "ディスクの健康状態",
"diskIgnorePath": "無視されたディスクパス", "diskIgnorePath": "無視されたディスクパス",
"displayCpuIndex": "CPUインデックスを表示する", "displayCpuIndex": "CPUインデックスを表示する",
"dl2Local": "{fileName}をローカルにダウンロードしますか?", "dl2Local": "{fileName}をローカルにダウンロードしますか?",

View File

@@ -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?",

View File

@@ -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?",

View File

@@ -36,6 +36,7 @@
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста", "dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
"disconnected": "Отключено", "disconnected": "Отключено",
"disk": "Диск", "disk": "Диск",
"diskHealth": "Состояние диска",
"diskIgnorePath": "Игнорировать путь к диску", "diskIgnorePath": "Игнорировать путь к диску",
"displayCpuIndex": "Отобразить индекс ЦП", "displayCpuIndex": "Отобразить индекс ЦП",
"dl2Local": "Загрузить {fileName} на локальный диск?", "dl2Local": "Загрузить {fileName} на локальный диск?",

View File

@@ -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?",

View File

@@ -36,6 +36,7 @@
"dirEmpty": "Переконайтеся, що директорія пуста.", "dirEmpty": "Переконайтеся, що директорія пуста.",
"disconnected": "Відключено", "disconnected": "Відключено",
"disk": "Диск", "disk": "Диск",
"diskHealth": "Стан диска",
"diskIgnorePath": "Ігнорувати шлях для диска", "diskIgnorePath": "Ігнорувати шлях для диска",
"displayCpuIndex": "Відобразити індекс ЦП", "displayCpuIndex": "Відобразити індекс ЦП",
"dl2Local": "Завантажити {fileName} на локальний комп'ютер?", "dl2Local": "Завантажити {fileName} на локальний комп'ютер?",

View File

@@ -36,6 +36,7 @@
"dirEmpty": "请确保文件夹为空", "dirEmpty": "请确保文件夹为空",
"disconnected": "连接断开", "disconnected": "连接断开",
"disk": "磁盘", "disk": "磁盘",
"diskHealth": "磁盘健康",
"diskIgnorePath": "忽略的磁盘路径", "diskIgnorePath": "忽略的磁盘路径",
"displayCpuIndex": "显示 CPU 索引", "displayCpuIndex": "显示 CPU 索引",
"dl2Local": "下载 {fileName} 到本地?", "dl2Local": "下载 {fileName} 到本地?",

View File

@@ -36,6 +36,7 @@
"dirEmpty": "請確保資料夾為空", "dirEmpty": "請確保資料夾為空",
"disconnected": "連接斷開", "disconnected": "連接斷開",
"disk": "磁碟", "disk": "磁碟",
"diskHealth": "磁碟健康",
"diskIgnorePath": "忽略的磁碟路徑", "diskIgnorePath": "忽略的磁碟路徑",
"displayCpuIndex": "顯示 CPU 索引", "displayCpuIndex": "顯示 CPU 索引",
"dl2Local": "下載 {fileName} 到本地?", "dl2Local": "下載 {fileName} 到本地?",

View File

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

View File

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

View File

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