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),
|
||||
gpu(Bootstrap.gpu_card),
|
||||
disk(Bootstrap.device_hdd_fill),
|
||||
smart(Icons.health_and_safety, sinceBuild: 1174),
|
||||
net(ZondIcons.network),
|
||||
sensor(MingCute.dashboard_4_line),
|
||||
temp(FontAwesome.temperature_empty_solid),
|
||||
battery(Icons.battery_full),
|
||||
pve(BoxIcons.bxs_dashboard, sinceBuild: 818),
|
||||
custom(Icons.code, sinceBuild: 825),
|
||||
;
|
||||
custom(Icons.code, sinceBuild: 825);
|
||||
|
||||
final int? sinceBuild;
|
||||
|
||||
@@ -31,19 +31,20 @@ enum ServerDetailCards {
|
||||
static final names = values.map((e) => e.name).toList();
|
||||
|
||||
String get toStr => switch (this) {
|
||||
about => libL10n.about,
|
||||
cpu => 'CPU',
|
||||
mem => 'RAM',
|
||||
swap => 'Swap',
|
||||
gpu => 'GPU',
|
||||
disk => l10n.disk,
|
||||
net => l10n.net,
|
||||
sensor => l10n.sensors,
|
||||
temp => l10n.temperature,
|
||||
battery => l10n.battery,
|
||||
pve => 'PVE',
|
||||
custom => l10n.cmd,
|
||||
};
|
||||
about => libL10n.about,
|
||||
cpu => 'CPU',
|
||||
mem => 'RAM',
|
||||
swap => 'Swap',
|
||||
gpu => 'GPU',
|
||||
disk => l10n.disk,
|
||||
smart => l10n.diskHealth,
|
||||
net => l10n.net,
|
||||
sensor => l10n.sensors,
|
||||
temp => l10n.temperature,
|
||||
battery => l10n.battery,
|
||||
pve => 'PVE',
|
||||
custom => l10n.cmd,
|
||||
};
|
||||
|
||||
/// If:
|
||||
/// Version 1 => user set [about], default is [about, cpu]
|
||||
|
||||
@@ -9,8 +9,7 @@ enum ShellFunc {
|
||||
process,
|
||||
shutdown,
|
||||
reboot,
|
||||
suspend,
|
||||
;
|
||||
suspend;
|
||||
|
||||
static const seperator = 'SrvBoxSep';
|
||||
|
||||
@@ -29,7 +28,9 @@ enum ShellFunc {
|
||||
/// Default is [scriptDirTmp]/[scriptFile], if this path is not accessible,
|
||||
/// it will be changed to [scriptDirHome]/[scriptFile].
|
||||
static String getScriptDir(String id) {
|
||||
final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir;
|
||||
final customScriptDir = ServerProvider.pick(
|
||||
id: id,
|
||||
)?.value.spi.custom?.scriptDir;
|
||||
if (customScriptDir != null) return customScriptDir;
|
||||
return _scriptDirMap.putIfAbsent(id, () {
|
||||
return scriptDirTmp;
|
||||
@@ -37,10 +38,10 @@ enum ShellFunc {
|
||||
}
|
||||
|
||||
static void switchScriptDir(String id) => switch (_scriptDirMap[id]) {
|
||||
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
|
||||
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
||||
_ => _scriptDirMap[id] = scriptDirHome,
|
||||
};
|
||||
scriptDirTmp => _scriptDirMap[id] = scriptDirHome,
|
||||
scriptDirHome => _scriptDirMap[id] = scriptDirTmp,
|
||||
_ => _scriptDirMap[id] = scriptDirHome,
|
||||
};
|
||||
|
||||
static String getScriptPath(String id) {
|
||||
return '${getScriptDir(id)}/$scriptFile';
|
||||
@@ -57,13 +58,13 @@ chmod 755 $scriptPath
|
||||
}
|
||||
|
||||
String get flag => switch (this) {
|
||||
ShellFunc.process => 'p',
|
||||
ShellFunc.shutdown => 'sd',
|
||||
ShellFunc.reboot => 'r',
|
||||
ShellFunc.suspend => 'sp',
|
||||
ShellFunc.status => 's',
|
||||
// ShellFunc.docker=> 'd',
|
||||
};
|
||||
ShellFunc.process => 'p',
|
||||
ShellFunc.shutdown => 'sd',
|
||||
ShellFunc.reboot => 'r',
|
||||
ShellFunc.suspend => 'sp',
|
||||
ShellFunc.status => 's',
|
||||
// ShellFunc.docker=> 'd',
|
||||
};
|
||||
|
||||
String exec(String id) => 'sh ${getScriptPath(id)} -$flag';
|
||||
|
||||
@@ -94,14 +95,14 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
else
|
||||
\t${BSDStatusCmdType.values.map((e) => e.cmd).join(cmdDivider)}
|
||||
fi''';
|
||||
// case ShellFunc.docker:
|
||||
// return '''
|
||||
// result=\$(docker version 2>&1 | grep "permission denied")
|
||||
// if [ "\$result" != "" ]; then
|
||||
// \t${_dockerCmds.join(_cmdDivider)}
|
||||
// else
|
||||
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
|
||||
// fi''';
|
||||
// case ShellFunc.docker:
|
||||
// return '''
|
||||
// result=\$(docker version 2>&1 | grep "permission denied")
|
||||
// if [ "\$result" != "" ]; then
|
||||
// \t${_dockerCmds.join(_cmdDivider)}
|
||||
// else
|
||||
// \t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)}
|
||||
// fi''';
|
||||
case ShellFunc.process:
|
||||
return '''
|
||||
if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
|
||||
@@ -162,7 +163,9 @@ exec 2>/dev/null
|
||||
// Write each func
|
||||
for (final func in values) {
|
||||
final customCmdsStr = () {
|
||||
if (func == ShellFunc.status && customCmds != null && customCmds.isNotEmpty) {
|
||||
if (func == ShellFunc.status &&
|
||||
customCmds != null &&
|
||||
customCmds.isNotEmpty) {
|
||||
return '$cmdDivider\n\t${customCmds.values.join(cmdDivider)}';
|
||||
}
|
||||
return '';
|
||||
@@ -209,17 +212,21 @@ enum StatusCmdType {
|
||||
cpu._('cat /proc/stat | grep cpu'),
|
||||
uptime._('uptime'),
|
||||
conn._('cat /proc/net/snmp'),
|
||||
disk._('lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID'),
|
||||
disk._(
|
||||
'lsblk --bytes --json --output FSTYPE,PATH,NAME,KNAME,MOUNTPOINT,FSSIZE,FSUSED,FSAVAIL,FSUSE%,UUID',
|
||||
),
|
||||
mem._("cat /proc/meminfo | grep -E 'Mem|Swap'"),
|
||||
tempType._('cat /sys/class/thermal/thermal_zone*/type'),
|
||||
tempVal._('cat /sys/class/thermal/thermal_zone*/temp'),
|
||||
host._('cat /etc/hostname'),
|
||||
diskio._('cat /proc/diskstats'),
|
||||
battery._('for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done'),
|
||||
battery._(
|
||||
'for f in /sys/class/power_supply/*/uevent; do cat "\$f"; echo; done',
|
||||
),
|
||||
nvidia._('nvidia-smi -q -x'),
|
||||
sensors._('sensors'),
|
||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"'),
|
||||
;
|
||||
diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -j /dev/\$d; echo; done'),
|
||||
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
|
||||
|
||||
final String cmd;
|
||||
|
||||
@@ -238,8 +245,7 @@ enum BSDStatusCmdType {
|
||||
mem._('top -l 1 | grep PhysMem'),
|
||||
//temp,
|
||||
host._('hostname'),
|
||||
cpuBrand._('sysctl -n machdep.cpu.brand_string'),
|
||||
;
|
||||
cpuBrand._('sysctl -n machdep.cpu.brand_string');
|
||||
|
||||
final String cmd;
|
||||
|
||||
@@ -248,10 +254,10 @@ enum BSDStatusCmdType {
|
||||
|
||||
extension StatusCmdTypeX on StatusCmdType {
|
||||
String get i18n => switch (this) {
|
||||
StatusCmdType.sys => l10n.system,
|
||||
StatusCmdType.host => l10n.host,
|
||||
StatusCmdType.uptime => l10n.uptime,
|
||||
StatusCmdType.battery => l10n.battery,
|
||||
final val => val.name,
|
||||
};
|
||||
StatusCmdType.sys => l10n.system,
|
||||
StatusCmdType.host => l10n.host,
|
||||
StatusCmdType.uptime => l10n.uptime,
|
||||
StatusCmdType.battery => l10n.battery,
|
||||
final val => val.name,
|
||||
};
|
||||
}
|
||||
|
||||
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/cpu.dart';
|
||||
import 'package:server_box/data/model/server/disk.dart';
|
||||
import 'package:server_box/data/model/server/disk_smart.dart';
|
||||
import 'package:server_box/data/model/server/memory.dart';
|
||||
import 'package:server_box/data/model/server/net_speed.dart';
|
||||
import 'package:server_box/data/model/server/nvdia.dart';
|
||||
@@ -19,12 +20,7 @@ class Server {
|
||||
SSHClient? client;
|
||||
ServerConn conn;
|
||||
|
||||
Server(
|
||||
this.spi,
|
||||
this.status,
|
||||
this.conn, {
|
||||
this.client,
|
||||
});
|
||||
Server(this.spi, this.status, this.conn, {this.client});
|
||||
|
||||
bool get needGenClient => conn < ServerConn.connecting;
|
||||
|
||||
@@ -44,6 +40,7 @@ class ServerStatus {
|
||||
SystemType system;
|
||||
Err? err;
|
||||
DiskIO diskIO;
|
||||
List<DiskSmart> diskSmart;
|
||||
List<NvidiaSmiItem>? nvidia;
|
||||
final List<Battery> batteries = [];
|
||||
final Map<StatusCmdType, String> more = {};
|
||||
@@ -61,6 +58,7 @@ class ServerStatus {
|
||||
required this.temps,
|
||||
required this.system,
|
||||
required this.diskIO,
|
||||
this.diskSmart = const [],
|
||||
this.err,
|
||||
this.nvidia,
|
||||
this.diskUsage,
|
||||
|
||||
@@ -85,7 +85,9 @@ extension Spix on Spi {
|
||||
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
|
||||
|
||||
bool shouldReconnect(Spi old) {
|
||||
return id != old.id ||
|
||||
return user != old.user ||
|
||||
ip != old.ip ||
|
||||
port != old.port ||
|
||||
pwd != old.pwd ||
|
||||
keyId != old.keyId ||
|
||||
alterUrl != old.alterUrl ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:server_box/data/model/server/battery.dart';
|
||||
import 'package:server_box/data/model/server/conn.dart';
|
||||
import 'package:server_box/data/model/server/cpu.dart';
|
||||
import 'package:server_box/data/model/server/disk.dart';
|
||||
import 'package:server_box/data/model/server/disk_smart.dart';
|
||||
import 'package:server_box/data/model/server/memory.dart';
|
||||
import 'package:server_box/data/model/server/net_speed.dart';
|
||||
import 'package:server_box/data/model/server/nvdia.dart';
|
||||
@@ -37,7 +38,8 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
|
||||
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
final segments = req.segments;
|
||||
|
||||
final time = int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||
final time =
|
||||
int.tryParse(StatusCmdType.time.find(segments)) ??
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
try {
|
||||
@@ -48,9 +50,7 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
}
|
||||
|
||||
try {
|
||||
final sys = _parseSysVer(
|
||||
StatusCmdType.sys.find(segments),
|
||||
);
|
||||
final sys = _parseSysVer(StatusCmdType.sys.find(segments));
|
||||
if (sys != null) {
|
||||
req.ss.more[StatusCmdType.sys] = sys;
|
||||
}
|
||||
@@ -130,6 +130,13 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
final smarts = DiskSmart.parse(StatusCmdType.diskSmart.find(segments));
|
||||
req.ss.diskSmart = smarts;
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning(e, s);
|
||||
}
|
||||
|
||||
try {
|
||||
req.ss.nvidia = NvidiaSmi.fromXml(StatusCmdType.nvidia.find(segments));
|
||||
} catch (e, s) {
|
||||
|
||||
@@ -45,21 +45,20 @@ class ServerProvider extends Provider {
|
||||
for (int idx = 0; idx < spis.length; idx++) {
|
||||
final spi = spis[idx];
|
||||
final originServer = oldServers[spi.id];
|
||||
final newServer = genServer(spi);
|
||||
|
||||
/// #258
|
||||
/// If not [shouldReconnect], then keep the old state.
|
||||
if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) {
|
||||
newServer.conn = originServer.value.conn;
|
||||
originServer.value.spi = spi;
|
||||
servers[spi.id] = originServer;
|
||||
} else {
|
||||
final newServer = genServer(spi);
|
||||
servers[spi.id] = newServer.vn;
|
||||
}
|
||||
servers[spi.id] = newServer.vn;
|
||||
}
|
||||
final serverOrder_ = Stores.setting.serverOrder.fetch();
|
||||
if (serverOrder_.isNotEmpty) {
|
||||
spis.reorder(
|
||||
order: serverOrder_,
|
||||
finder: (n, id) => n.id == id,
|
||||
);
|
||||
spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id);
|
||||
serverOrder.value.addAll(spis.map((e) => e.id));
|
||||
} else {
|
||||
serverOrder.value.addAll(servers.keys);
|
||||
@@ -104,31 +103,30 @@ class ServerProvider extends Provider {
|
||||
|
||||
/// if [spi] is specificed then only refresh this server
|
||||
/// [onlyFailed] only refresh failed servers
|
||||
static Future<void> refresh({
|
||||
Spi? spi,
|
||||
bool onlyFailed = false,
|
||||
}) async {
|
||||
static Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
|
||||
if (spi != null) {
|
||||
_manualDisconnectedIds.remove(spi.id);
|
||||
await _getData(spi);
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(servers.values.map((val) async {
|
||||
final s = val.value;
|
||||
if (onlyFailed) {
|
||||
if (s.conn != ServerConn.failed) return;
|
||||
TryLimiter.reset(s.spi.id);
|
||||
}
|
||||
await Future.wait(
|
||||
servers.values.map((val) async {
|
||||
final s = val.value;
|
||||
if (onlyFailed) {
|
||||
if (s.conn != ServerConn.failed) return;
|
||||
TryLimiter.reset(s.spi.id);
|
||||
}
|
||||
|
||||
if (_manualDisconnectedIds.contains(s.spi.id)) return;
|
||||
if (_manualDisconnectedIds.contains(s.spi.id)) return;
|
||||
|
||||
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
|
||||
return;
|
||||
}
|
||||
if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await _getData(s.spi);
|
||||
}));
|
||||
return await _getData(s.spi);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> startAutoRefresh() async {
|
||||
@@ -307,14 +305,11 @@ class ServerProvider extends Provider {
|
||||
_setServerState(s, ServerConn.connected);
|
||||
|
||||
try {
|
||||
final (_, writeScriptResult) = await sv.client!.exec(
|
||||
(session) async {
|
||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
},
|
||||
entry: ShellFunc.getInstallShellCmd(spi.id),
|
||||
);
|
||||
final (_, writeScriptResult) = await sv.client!.exec((session) async {
|
||||
final scriptRaw = ShellFunc.allScript(spi.custom?.cmds).uint8List;
|
||||
session.stdin.add(scriptRaw);
|
||||
session.stdin.close();
|
||||
}, entry: ShellFunc.getInstallShellCmd(spi.id));
|
||||
if (writeScriptResult.isNotEmpty) {
|
||||
ShellFunc.switchScriptDir(spi.id);
|
||||
throw writeScriptResult;
|
||||
@@ -366,10 +361,7 @@ class ServerProvider extends Provider {
|
||||
}
|
||||
}
|
||||
TryLimiter.inc(sid);
|
||||
sv.status.err = SSHErr(
|
||||
type: SSHErrType.segements,
|
||||
message: 'Seperate segments failed, raw:\n$raw',
|
||||
);
|
||||
sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw');
|
||||
_setServerState(s, ServerConn.failed);
|
||||
return;
|
||||
}
|
||||
@@ -408,17 +400,10 @@ class ServerProvider extends Provider {
|
||||
system: systemType,
|
||||
customCmds: spi.custom?.cmds ?? {},
|
||||
);
|
||||
sv.status = await Computer.shared.start(
|
||||
getStatus,
|
||||
req,
|
||||
taskName: 'StatusUpdateReq<${sv.id}>',
|
||||
);
|
||||
sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>');
|
||||
} catch (e, trace) {
|
||||
TryLimiter.inc(sid);
|
||||
sv.status.err = SSHErr(
|
||||
type: SSHErrType.getStatus,
|
||||
message: 'Parse failed: $e\n\n$raw',
|
||||
);
|
||||
sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
|
||||
_setServerState(s, ServerConn.failed);
|
||||
Loggers.app.warning('Server status', e, trace);
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,11 @@ abstract final class GithubIds {
|
||||
'dccif',
|
||||
'mikropsoft',
|
||||
'CakesTwix',
|
||||
'dsvf',
|
||||
'fei1025',
|
||||
'MasedMSD',
|
||||
'GitGitro',
|
||||
'Shin-suechtig',
|
||||
};
|
||||
|
||||
static const participants = <GhId>{
|
||||
@@ -99,6 +104,20 @@ abstract final class GithubIds {
|
||||
'88484396',
|
||||
'honggeigei',
|
||||
'likecreep',
|
||||
'axlrose',
|
||||
'immortal521',
|
||||
'PRO-2684',
|
||||
'Xiaobao-Yang',
|
||||
'Mrhs121',
|
||||
'Fudiautobi',
|
||||
'papaj-na-wrotkach',
|
||||
'kid1412621',
|
||||
'smanx',
|
||||
'xuanyue1024',
|
||||
'RuofengX',
|
||||
'rhwong',
|
||||
'AstroEngineeer',
|
||||
'mochasweet',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,56 +8,33 @@ import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/model/server/temp.dart';
|
||||
|
||||
abstract final class InitStatus {
|
||||
static SingleCpuCore get _initOneTimeCpuStatus => SingleCpuCore(
|
||||
'cpu',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
static Cpus get cpus => Cpus(
|
||||
[_initOneTimeCpuStatus],
|
||||
[_initOneTimeCpuStatus],
|
||||
);
|
||||
static NetSpeedPart get _initNetSpeedPart => NetSpeedPart(
|
||||
'',
|
||||
BigInt.zero,
|
||||
BigInt.zero,
|
||||
0,
|
||||
);
|
||||
static NetSpeed get netSpeed => NetSpeed(
|
||||
[_initNetSpeedPart],
|
||||
[_initNetSpeedPart],
|
||||
);
|
||||
static SingleCpuCore get _initOneTimeCpuStatus =>
|
||||
SingleCpuCore('cpu', 0, 0, 0, 0, 0, 0, 0);
|
||||
static Cpus get cpus =>
|
||||
Cpus([_initOneTimeCpuStatus], [_initOneTimeCpuStatus]);
|
||||
static NetSpeedPart get _initNetSpeedPart =>
|
||||
NetSpeedPart('', BigInt.zero, BigInt.zero, 0);
|
||||
static NetSpeed get netSpeed =>
|
||||
NetSpeed([_initNetSpeedPart], [_initNetSpeedPart]);
|
||||
static ServerStatus get status => ServerStatus(
|
||||
cpu: cpus,
|
||||
mem: const Memory(
|
||||
total: 1,
|
||||
free: 1,
|
||||
avail: 1,
|
||||
),
|
||||
disk: [
|
||||
Disk(
|
||||
path: '/',
|
||||
mount: '/',
|
||||
usedPercent: 0,
|
||||
used: BigInt.zero,
|
||||
size: BigInt.one,
|
||||
avail: BigInt.zero,
|
||||
)
|
||||
],
|
||||
tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
|
||||
netSpeed: netSpeed,
|
||||
swap: const Swap(
|
||||
total: 0,
|
||||
free: 0,
|
||||
cached: 0,
|
||||
),
|
||||
system: SystemType.linux,
|
||||
temps: Temperatures(),
|
||||
diskIO: DiskIO([], []),
|
||||
);
|
||||
cpu: cpus,
|
||||
mem: const Memory(total: 1, free: 1, avail: 1),
|
||||
disk: [
|
||||
Disk(
|
||||
path: '/',
|
||||
mount: '/',
|
||||
usedPercent: 0,
|
||||
used: BigInt.zero,
|
||||
size: BigInt.one,
|
||||
avail: BigInt.zero,
|
||||
),
|
||||
],
|
||||
tcp: const Conn(maxConn: 0, active: 0, passive: 0, fail: 0),
|
||||
netSpeed: netSpeed,
|
||||
swap: const Swap(total: 0, free: 0, cached: 0),
|
||||
system: SystemType.linux,
|
||||
temps: Temperatures(),
|
||||
diskIO: DiskIO([], []),
|
||||
diskSmart: const [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -335,6 +335,12 @@ abstract class AppLocalizations {
|
||||
/// **'Disk'**
|
||||
String get disk;
|
||||
|
||||
/// No description provided for @diskHealth.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Disk Health'**
|
||||
String get diskHealth;
|
||||
|
||||
/// No description provided for @diskIgnorePath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -128,6 +128,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Festplatte';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Festplattengesundheit';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Pfad für Datenträger ignorieren';
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Disk';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Disk Health';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Ignore path for disk';
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Disco';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Salud del disco';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Rutas de disco ignoradas';
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Disque';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Santé du disque';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Chemin à ignorer pour le disque';
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Disk';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Kesehatan disk';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Abaikan jalan untuk disk';
|
||||
|
||||
|
||||
@@ -120,6 +120,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'ディスク';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'ディスクの健康状態';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => '無視されたディスクパス';
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Schijf';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Schijfgezondheid';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Pad negeren voor schijf';
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Disco';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Saúde do disco';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Caminhos de disco ignorados';
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Диск';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Состояние диска';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Игнорировать путь к диску';
|
||||
|
||||
|
||||
@@ -126,6 +126,9 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Disk';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Disk sağlığı';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Disk için yok sayılan yol';
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get disk => 'Диск';
|
||||
|
||||
@override
|
||||
String get diskHealth => 'Стан диска';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => 'Ігнорувати шлях для диска';
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get disk => '磁盘';
|
||||
|
||||
@override
|
||||
String get diskHealth => '磁盘健康';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => '忽略的磁盘路径';
|
||||
|
||||
@@ -844,6 +847,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
@override
|
||||
String get disk => '磁碟';
|
||||
|
||||
@override
|
||||
String get diskHealth => '磁碟健康';
|
||||
|
||||
@override
|
||||
String get diskIgnorePath => '忽略的磁碟路徑';
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Stelle sicher, dass der Ordner leer ist.",
|
||||
"disconnected": "Disconnected",
|
||||
"disk": "Festplatte",
|
||||
"diskHealth": "Festplattengesundheit",
|
||||
"diskIgnorePath": "Pfad für Datenträger ignorieren",
|
||||
"displayCpuIndex": "Zeigen Sie den CPU-Index an",
|
||||
"dl2Local": "Datei \"{fileName}\" herunterladen?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Make sure the folder is empty.",
|
||||
"disconnected": "Disconnected",
|
||||
"disk": "Disk",
|
||||
"diskHealth": "Disk Health",
|
||||
"diskIgnorePath": "Ignore path for disk",
|
||||
"displayCpuIndex": "Display CPU index",
|
||||
"dl2Local": "Download {fileName} to local?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Asegúrate de que el directorio esté vacío",
|
||||
"disconnected": "Desconectado",
|
||||
"disk": "Disco",
|
||||
"diskHealth": "Salud del disco",
|
||||
"diskIgnorePath": "Rutas de disco ignoradas",
|
||||
"displayCpuIndex": "Muestre el índice de CPU",
|
||||
"dl2Local": "¿Descargar {fileName} a local?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Assurez-vous que le répertoire est vide.",
|
||||
"disconnected": "Déconnecté",
|
||||
"disk": "Disque",
|
||||
"diskHealth": "Santé du disque",
|
||||
"diskIgnorePath": "Chemin à ignorer pour le disque",
|
||||
"displayCpuIndex": "Afficher l'index CPU",
|
||||
"dl2Local": "Télécharger {fileName} localement ?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Pastikan dir kosong.",
|
||||
"disconnected": "Terputus",
|
||||
"disk": "Disk",
|
||||
"diskHealth": "Kesehatan disk",
|
||||
"diskIgnorePath": "Abaikan jalan untuk disk",
|
||||
"displayCpuIndex": "Tampilkan indeks CPU",
|
||||
"dl2Local": "Unduh {fileName} ke lokal?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "フォルダーが空であることを確認してください",
|
||||
"disconnected": "接続が切断されました",
|
||||
"disk": "ディスク",
|
||||
"diskHealth": "ディスクの健康状態",
|
||||
"diskIgnorePath": "無視されたディスクパス",
|
||||
"displayCpuIndex": "CPUインデックスを表示する",
|
||||
"dl2Local": "{fileName}をローカルにダウンロードしますか?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Zorg ervoor dat de map leeg is.",
|
||||
"disconnected": "Verbroken",
|
||||
"disk": "Schijf",
|
||||
"diskHealth": "Schijfgezondheid",
|
||||
"diskIgnorePath": "Pad negeren voor schijf",
|
||||
"displayCpuIndex": "Toon de CPU-index",
|
||||
"dl2Local": "Download {fileName} naar lokaal?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Certifique-se de que a pasta está vazia",
|
||||
"disconnected": "Desconectado",
|
||||
"disk": "Disco",
|
||||
"diskHealth": "Saúde do disco",
|
||||
"diskIgnorePath": "Caminhos de disco ignorados",
|
||||
"displayCpuIndex": "Exiba o índice de CPU",
|
||||
"dl2Local": "Baixar {fileName} para o local?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Пожалуйста, убедитесь, что папка пуста",
|
||||
"disconnected": "Отключено",
|
||||
"disk": "Диск",
|
||||
"diskHealth": "Состояние диска",
|
||||
"diskIgnorePath": "Игнорировать путь к диску",
|
||||
"displayCpuIndex": "Отобразить индекс ЦП",
|
||||
"dl2Local": "Загрузить {fileName} на локальный диск?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Klasörün boş olduğundan emin olun.",
|
||||
"disconnected": "Bağlantı kesildi",
|
||||
"disk": "Disk",
|
||||
"diskHealth": "Disk sağlığı",
|
||||
"diskIgnorePath": "Disk için yok sayılan yol",
|
||||
"displayCpuIndex": "CPU indeksini göster",
|
||||
"dl2Local": "{fileName} dosyasını yerel cihaza indir?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "Переконайтеся, що директорія пуста.",
|
||||
"disconnected": "Відключено",
|
||||
"disk": "Диск",
|
||||
"diskHealth": "Стан диска",
|
||||
"diskIgnorePath": "Ігнорувати шлях для диска",
|
||||
"displayCpuIndex": "Відобразити індекс ЦП",
|
||||
"dl2Local": "Завантажити {fileName} на локальний комп'ютер?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "请确保文件夹为空",
|
||||
"disconnected": "连接断开",
|
||||
"disk": "磁盘",
|
||||
"diskHealth": "磁盘健康",
|
||||
"diskIgnorePath": "忽略的磁盘路径",
|
||||
"displayCpuIndex": "显示 CPU 索引",
|
||||
"dl2Local": "下载 {fileName} 到本地?",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dirEmpty": "請確保資料夾為空",
|
||||
"disconnected": "連接斷開",
|
||||
"disk": "磁碟",
|
||||
"diskHealth": "磁碟健康",
|
||||
"diskIgnorePath": "忽略的磁碟路徑",
|
||||
"displayCpuIndex": "顯示 CPU 索引",
|
||||
"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/cpu.dart';
|
||||
import 'package:server_box/data/model/server/disk.dart';
|
||||
import 'package:server_box/data/model/server/disk_smart.dart';
|
||||
import 'package:server_box/data/model/server/dist.dart';
|
||||
import 'package:server_box/data/model/server/net_speed.dart';
|
||||
import 'package:server_box/data/model/server/nvdia.dart';
|
||||
@@ -43,6 +44,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
_buildSwapView,
|
||||
_buildGpuView,
|
||||
_buildDiskView,
|
||||
_buildDiskSmart,
|
||||
_buildNetView,
|
||||
_buildSensors,
|
||||
_buildTemperature,
|
||||
@@ -147,7 +149,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerPr
|
||||
return ExtendedImage.network(
|
||||
logoUrl,
|
||||
cache: true,
|
||||
height: cons.maxHeight * 0.2,
|
||||
height: cons.maxWidth * 0.3,
|
||||
width: cons.maxWidth,
|
||||
);
|
||||
},
|
||||
@@ -563,6 +565,55 @@ class _ServerDetailPageState extends State<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) {
|
||||
final ss = si.status;
|
||||
final ns = ss.netSpeed;
|
||||
|
||||
Reference in New Issue
Block a user