From c42c701ffc5cc65adf2600c968f63ec92d2ae950 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?=
=?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com>
Date: Wed, 11 Jun 2025 16:45:25 +0800
Subject: [PATCH] bug: incorrect disk smart info (#789)
---
README.md | 7 +-
README_zh.md | 21 +++--
lib/data/model/app/shell_func.dart | 2 +-
lib/data/model/server/disk_smart.dart | 91 +++++++++++++++++-
lib/view/page/server/detail/view.dart | 131 ++++++++++++++++++++++----
pubspec.lock | 4 +-
pubspec.yaml | 2 +-
7 files changed, 223 insertions(+), 35 deletions(-)
diff --git a/README.md b/README.md
index aa43afe8..752a3ccc 100644
--- a/README.md
+++ b/README.md
@@ -28,8 +28,8 @@ Especially thanks to dartss
## 📥 Install
-| Platform | From |
-|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+|Platform| From|
+|--|--|
| iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) |
| Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) |
| Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) |
@@ -38,13 +38,14 @@ Please only download pkgs from the source that **you trust**!
## 🔖 Feature
-- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`...
+- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`...
- Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`...
- English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT)
## 🆘 Help
diff --git a/README_zh.md b/README_zh.md
index a471d0fb..0bfeddfc 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -15,8 +15,8 @@
特别感谢 dartssh2 & xterm.dart。
-
## 🏙️ 截屏
+
 |
@@ -26,20 +26,19 @@
-
## 📥 安装
-平台 | 下载
-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+平台|下载
+--|--
iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703)
Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/)
Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid)
请从 **信任** 的来源下载!
-
## 🔖 特点
-- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理...
+
+- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理,`S.M.A.R.T`...
- 特殊支持:`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`...
- 本地化
- English, 简体中文
@@ -47,10 +46,10 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
- Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix);
- 感谢贡献者们!
-
## 🆘 帮助
@@ -59,26 +58,30 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
- **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。
反馈前须知:
+
1. 反馈问题请附带 log(点击首页右上角),并以 bug 模版提交。
2. 反馈问题前请检查是否是 serverbox 的问题。
3. 欢迎所有有效、正面的反馈,主观(比如你觉得其他UI更好看)的反馈不一定会接受
-
## 🧱 贡献
+
任何正面的贡献都欢迎。
### 开发
+
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
2. 克隆这个仓库, 运行 `flutter run` 启动应用
3. 运行 `dart run fl_build -p PLATFORM` 构建应用
### 翻译
+
[指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。
## 💡 我的其它 Apps
+
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。
- [更多](https://github.com/lollipopkit) - 工具 & etc.
-
## 📝 协议
+
`GPL v3 lollipopkit`
diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart
index bbdf4e2a..2fca8db6 100644
--- a/lib/data/model/app/shell_func.dart
+++ b/lib/data/model/app/shell_func.dart
@@ -225,7 +225,7 @@ enum StatusCmdType {
),
nvidia._('nvidia-smi -q -x'),
sensors._('sensors'),
- diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -j /dev/\$d; echo; done'),
+ diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'),
cpuBrand._('cat /proc/cpuinfo | grep "model name"');
final String cmd;
diff --git a/lib/data/model/server/disk_smart.dart b/lib/data/model/server/disk_smart.dart
index d2cc065a..78595454 100644
--- a/lib/data/model/server/disk_smart.dart
+++ b/lib/data/model/server/disk_smart.dart
@@ -35,7 +35,10 @@ abstract class DiskSmart with _$DiskSmart {
// Basic
final device = data['device']?['name']?.toString() ?? '';
- final healthy = data['smart_status']?['passed'] as bool?;
+
+ if (!_isPhysicalDisk(device)) continue;
+
+ final healthy = _parseHealthStatus(data);
// Model and Serial
final model =
@@ -72,6 +75,92 @@ abstract class DiskSmart with _$DiskSmart {
return results;
}
+ static bool _isPhysicalDisk(String device) {
+ if (device.isEmpty) return false;
+
+ // Common patterns for physical disks
+ final patterns = [
+ RegExp(r'^/dev/sd[a-z]$'), // SATA/SCSI: /dev/sda, /dev/sdb
+ RegExp(r'^/dev/hd[a-z]$'), // IDE: /dev/hda, /dev/hdb
+ RegExp(r'^/dev/nvme\d+n\d+$'), // NVMe: /dev/nvme0n1, /dev/nvme1n1
+ RegExp(r'^/dev/mmcblk\d+$'), // MMC: /dev/mmcblk0
+ RegExp(r'^/dev/vd[a-z]$'), // VirtIO: /dev/vda, /dev/vdb
+ RegExp(r'^/dev/xvd[a-z]$'), // Xen: /dev/xvda, /dev/xvdb
+ ];
+
+ return patterns.any((pattern) => pattern.hasMatch(device));
+ }
+
+ static bool? _parseHealthStatus(Map data) {
+ // smart_status.passed
+ final smartStatus = data['smart_status'];
+ if (smartStatus is Map) {
+ final passed = smartStatus['passed'];
+ if (passed is bool) return passed;
+ }
+
+ // smart_status.status
+ if (smartStatus is Map) {
+ final status = smartStatus['status']?.toString().toLowerCase();
+ if (status != null) {
+ if (status.contains('pass') || status.contains('ok')) return true;
+ if (status.contains('fail')) return false;
+ }
+ }
+
+ // smart_status
+ final rootSmartStatus = data['smart_status']?.toString().toLowerCase();
+ if (rootSmartStatus != null) {
+ if (rootSmartStatus.contains('pass') || rootSmartStatus.contains('ok')) return true;
+ if (rootSmartStatus.contains('fail')) return false;
+ }
+
+ // health attrs
+ final attrTable = data['ata_smart_attributes']?['table'] as List?;
+ if (attrTable != null) {
+ var hasFailingAttributes = false;
+
+ for (final attr in attrTable) {
+ if (attr is Map) {
+ final whenFailed = attr['when_failed']?.toString();
+ if (whenFailed != null && whenFailed.isNotEmpty && whenFailed != 'never') {
+ hasFailingAttributes = true;
+ break;
+ }
+
+ // Whether the attribute is critical
+ final name = attr['name']?.toString();
+ final value = attr['value'] as int?;
+ final thresh = attr['thresh'] as int?;
+
+ if (name != null && value != null && thresh != null && thresh > 0) {
+ const criticalAttrs = [
+ 'Reallocated_Sector_Ct',
+ 'Reallocated_Event_Count',
+ 'Current_Pending_Sector',
+ 'Offline_Uncorrectable',
+ 'UDMA_CRC_Error_Count',
+ ];
+
+ if (criticalAttrs.contains(name) && value < thresh) {
+ hasFailingAttributes = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (hasFailingAttributes) return false;
+ }
+
+ if (attrTable != null && attrTable.isNotEmpty) {
+ return true;
+ }
+
+ // Uncertain status, assume healthy
+ return true;
+ }
+
static Map _parseSmartAttributes(Map data) {
final attributes = {};
diff --git a/lib/view/page/server/detail/view.dart b/lib/view/page/server/detail/view.dart
index 3b17ba1a..8c2d2916 100644
--- a/lib/view/page/server/detail/view.dart
+++ b/lib/view/page/server/detail/view.dart
@@ -580,38 +580,133 @@ class _ServerDetailPageState extends State with SingleTickerPr
}
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);
+ final healthStatus = _getDiskHealthStatus(smart);
return ListTile(
dense: true,
- contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
- leading: statusIcon,
+ leading: healthStatus.icon,
title: Text(smart.device, style: UIs.text13, textScaler: _textFactor),
trailing: Text(
- statusText,
- style: UIs.text13.copyWith(color: statusColor, fontWeight: FontWeight.bold),
+ healthStatus.text,
+ style: UIs.text13.copyWith(fontWeight: FontWeight.bold),
textScaler: _textFactor,
),
subtitle: _buildDiskSmartDetails(smart),
+ onTap: () => _onTapDiskSmartItem(smart),
);
}
+ ({String text, Color color, Widget icon}) _getDiskHealthStatus(DiskSmart smart) {
+ if (smart.healthy == null) {
+ return (
+ text: libL10n.unknown,
+ color: Colors.orange,
+ icon: const Icon(Icons.help_outline, color: Colors.orange, size: 18),
+ );
+ } else if (smart.healthy!) {
+ return (
+ text: 'PASS',
+ color: Colors.green,
+ icon: const Icon(Icons.check_circle, color: Colors.green, size: 18),
+ );
+ } else {
+ return (text: 'FAIL', color: Colors.red, icon: const Icon(Icons.error, color: Colors.red, size: 18));
+ }
+ }
+
Widget? _buildDiskSmartDetails(DiskSmart smart) {
final details = [];
-
- if (smart.model != null) details.add(smart.model!);
- if (smart.serial != null) details.add('S/N: ${smart.serial}');
- if (smart.temperature != null) details.add('${smart.temperature!.toStringAsFixed(1)}°C');
- if (smart.powerOnHours != null) details.add('${smart.powerOnHours} hours');
-
+
+ if (smart.model != null) {
+ details.add(smart.model!);
+ }
+
+ if (smart.temperature != null) {
+ details.add('${smart.temperature!.toStringAsFixed(1)}°C');
+ }
+
+ if (smart.powerOnHours != null) {
+ final hours = smart.powerOnHours!;
+ details.add('$hours ${libL10n.hour}');
+ }
+
+ if (smart.ssdLifeLeft != null) {
+ details.add('Life left: ${smart.ssdLifeLeft}%');
+ }
+
if (details.isEmpty) return null;
-
- return Text(details.join(' | '), style: UIs.text12, textScaler: _textFactor);
+
+ return Text(
+ details.join(' | '),
+ style: UIs.text12Grey,
+ textScaler: _textFactor,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ );
+ }
+
+ void _onTapDiskSmartItem(DiskSmart smart) {
+ final details = [];
+
+ if (smart.model != null) details.add('Model: ${smart.model}');
+ if (smart.serial != null) details.add('Serial: ${smart.serial}');
+ if (smart.temperature != null) details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C');
+
+ if (smart.powerOnHours != null) {
+ details.add('Power On: ${smart.powerOnHours} ${libL10n.hour}');
+ }
+ if (smart.powerCycleCount != null) {
+ details.add('Power Cycle: ${smart.powerCycleCount}');
+ }
+
+ if (smart.ssdLifeLeft != null) {
+ details.add('Life Left: ${smart.ssdLifeLeft}%');
+ }
+ if (smart.lifetimeWritesGiB != null) {
+ details.add('Lifetime Write: ${smart.lifetimeWritesGiB} GiB');
+ }
+ if (smart.lifetimeReadsGiB != null) {
+ details.add('Lifetime Read: ${smart.lifetimeReadsGiB} GiB');
+ }
+ if (smart.averageEraseCount != null) {
+ details.add('Avg. Erase: ${smart.averageEraseCount}');
+ }
+ if (smart.unsafeShutdownCount != null) {
+ details.add('Unsafe Shutdown: ${smart.unsafeShutdownCount}');
+ }
+
+ final criticalAttrs = [
+ 'Reallocated_Sector_Ct',
+ 'Current_Pending_Sector',
+ 'Offline_Uncorrectable',
+ 'UDMA_CRC_Error_Count',
+ ];
+
+ for (final attrName in criticalAttrs) {
+ final attr = smart.getAttribute(attrName);
+ if (attr != null && attr.rawValue != null) {
+ final value = attr.rawValue.toString();
+ details.add('${attrName.replaceAll('_', ' ')}: $value');
+ }
+ }
+
+ if (details.isEmpty) {
+ return;
+ }
+
+ final markdown = details.join('\n\n- ');
+ context.showRoundDialog(
+ title: smart.device,
+ child: MarkdownBody(
+ data: '- $markdown',
+ selectable: true,
+ styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
+ p: UIs.text13Grey,
+ h2: UIs.text15,
+ ),
+ ),
+ actions: Btnx.oks,
+ );
}
Widget? _buildNetView(Server si) {
diff --git a/pubspec.lock b/pubspec.lock
index 47366ee3..ceadc601 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -480,8 +480,8 @@ packages:
dependency: "direct dev"
description:
path: "."
- ref: "v1.0.49"
- resolved-ref: "830d1f4c95a35f440881b6ff9c57e24711002fa0"
+ ref: "v1.0.50"
+ resolved-ref: c7b53e0e6b40e4a021175695fdc9e7a54a9b5321
url: "https://github.com/lppcg/fl_build.git"
source: git
version: "1.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 6bcac6f8..4675b05b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -91,7 +91,7 @@ dev_dependencies:
fl_build:
git:
url: https://github.com/lppcg/fl_build.git
- ref: v1.0.49
+ ref: v1.0.50
flutter:
generate: true