From 6a1ffd7de2c8da5f44d7dcdc3fc1227ed7b4c802 Mon Sep 17 00:00:00 2001
From: franciscodimattia <59030809+franciscodimattia@users.noreply.github.com>
Date: Thu, 19 Feb 2026 16:12:03 -0300
Subject: [PATCH 1/2] Improved UI and show tooltip with extra info
---
.../src/opnsense/www/js/widgets/Smart.js | 103 +++++++++++++++---
1 file changed, 87 insertions(+), 16 deletions(-)
diff --git a/sysutils/smart/src/opnsense/www/js/widgets/Smart.js b/sysutils/smart/src/opnsense/www/js/widgets/Smart.js
index 30b2d5986..d395c14c8 100644
--- a/sysutils/smart/src/opnsense/www/js/widgets/Smart.js
+++ b/sysutils/smart/src/opnsense/www/js/widgets/Smart.js
@@ -28,34 +28,105 @@ export default class Smart extends BaseTableWidget {
constructor() {
super();
this.tickTimeout = 300;
- this.disks = null;
}
getMarkup() {
const $container = $('
`).text(text).css(css).prop('outerHTML');
- rows.push([[device.device], field]);
- } catch (error) {
- super.updateTable('smart-table', [[["Error"], $(`${this.translations.nosmart} ${device.device}: ${error}`).prop('outerHTML')]]);
- }
+ parseSmartAttributes(output) {
+ const attrs = {};
+ if (!output) return attrs;
+
+ const lines = output.split('\n');
+ lines.forEach(line => {
+ const match = line.match(/^\s*(\d{1,3})\s+([A-Za-z0-9_-]+(?:\s+[A-Za-z0-9_-]+)*)\s+0x[0-9a-f]+\s+\d+\s+\d+\s+\d+\s+.*\s+([-]?\d+)\s*$/);
+ if (match) {
+ const [, id, name, raw] = match;
+ attrs[id.trim()] = { name: name.trim(), raw: raw.trim() };
}
+ });
+ return attrs;
+ }
+
+ async onWidgetTick() {
+ try {
+ const listResp = await this.ajaxCall(`/api/smart/service/list/detailed`, {}, 'POST');
+ const devices = listResp?.devices || [];
+
+ const rows = [];
+ for (const dev of devices) {
+ const deviceName = dev.device || 'Unknown';
+ const ident = dev.ident || 'N/A';
+ let statusHtml = 'Unknown';
+ let tooltip = `Device: ${deviceName}\nSerial/Ident: ${ident}`;
+
+ try {
+ const state = dev.state || {};
+ const health = state.smart_status?.passed ?? null;
+
+ if (health !== null) {
+ const icon = health
+ ? ''
+ : '';
+ statusHtml = `${icon} ${health ? 'OK' : 'FAILED'}`;
+ }
+
+ const infoBody = JSON.stringify({
+ type: 'A',
+ device: deviceName
+ });
+
+ const infoResp = await this.ajaxCall(`/api/smart/service/info`, infoBody, 'POST');
+
+ if (infoResp?.output && typeof infoResp.output === 'string' && infoResp.output.trim()) {
+ const attrs = this.parseSmartAttributes(infoResp.output);
+
+ tooltip += '\n\nKey Attributes:';
+ if (attrs['194']) tooltip += `\nTemperature: ${attrs['194'].raw} °C`;
+ if (attrs['9']) tooltip += `\nPower-On Hours: ${attrs['9'].raw} h`;
+ if (attrs['5']) tooltip += `\nReallocated Sectors: ${attrs['5'].raw}`;
+ if (attrs['197']) tooltip += `\nCurrent Pending: ${attrs['197'].raw}`;
+ if (attrs['198']) tooltip += `\nOffline Uncorrectable: ${attrs['198'].raw}`;
+ if (attrs['199']) tooltip += `\nUDMA CRC Errors: ${attrs['199'].raw}`;
+ } else {
+ tooltip += '\n\n(No detailed attributes available)';
+ }
+
+ } catch (e) {
+ tooltip += `\n\nError fetching details: ${e.message || 'Unknown'}`;
+ }
+
+ const escapedTooltip = tooltip.replace(/"/g, '"').replace(/\n/g, '
');
+ const deviceHtml = `${deviceName}`;
+
+ rows.push([deviceHtml, statusHtml]);
+ }
+
+ rows.sort((a, b) => String(a[0]).localeCompare(String(b[0])));
super.updateTable('smart-table', rows);
+
+ $('[data-toggle="tooltip"]').tooltip({container: 'body', html: true});
+
+ } catch (err) {
+ super.updateTable('smart-table', [[
+ 'Widget error',
+ `${err.message || 'Unknown'}`
+ ]]);
}
}
}
From 189e1ea60ae4b58e4e3141db2d95b5ed3365fa6f Mon Sep 17 00:00:00 2001
From: franciscodimattia <59030809+franciscodimattia@users.noreply.github.com>
Date: Fri, 20 Feb 2026 13:35:15 -0300
Subject: [PATCH 2/2] Using info_json API
---
.../src/opnsense/www/js/widgets/Smart.js | 116 ++++++------------
1 file changed, 35 insertions(+), 81 deletions(-)
diff --git a/sysutils/smart/src/opnsense/www/js/widgets/Smart.js b/sysutils/smart/src/opnsense/www/js/widgets/Smart.js
index d395c14c8..03bf46f57 100644
--- a/sysutils/smart/src/opnsense/www/js/widgets/Smart.js
+++ b/sysutils/smart/src/opnsense/www/js/widgets/Smart.js
@@ -31,102 +31,56 @@ export default class Smart extends BaseTableWidget {
}
getMarkup() {
- const $container = $('');
-
- const headers = [
- this.translations?.device || 'Device',
- this.translations?.status || 'SMART Status'
- ];
-
- const $smarttable = this.createTable('smart-table', {
- headers: headers
+ const $table = this.createTable('smart-table', {
+ headers: ['Device', 'SMART Status']
});
-
- $smarttable.addClass('table table-striped table-condensed table-hover');
-
- $container.append($smarttable);
- return $container;
+ $table.addClass('table table-striped table-condensed table-hover');
+ return $('').append($table);
}
- parseSmartAttributes(output) {
- const attrs = {};
- if (!output) return attrs;
-
- const lines = output.split('\n');
- lines.forEach(line => {
- const match = line.match(/^\s*(\d{1,3})\s+([A-Za-z0-9_-]+(?:\s+[A-Za-z0-9_-]+)*)\s+0x[0-9a-f]+\s+\d+\s+\d+\s+\d+\s+.*\s+([-]?\d+)\s*$/);
- if (match) {
- const [, id, name, raw] = match;
- attrs[id.trim()] = { name: name.trim(), raw: raw.trim() };
- }
- });
- return attrs;
+ getAttrRaw(output, id) {
+ const attr = output?.ata_smart_attributes?.table?.find(a => a.id === id);
+ return attr?.raw?.string ?? 'N/A';
}
async onWidgetTick() {
try {
- const listResp = await this.ajaxCall(`/api/smart/service/list/detailed`, {}, 'POST');
- const devices = listResp?.devices || [];
+ const {devices = []} = await this.ajaxCall('/api/smart/service/list/detailed', {}, 'POST') || {};
- const rows = [];
- for (const dev of devices) {
- const deviceName = dev.device || 'Unknown';
+ const rows = await Promise.all(devices.map(async dev => {
+ const name = dev.device || 'Unknown';
const ident = dev.ident || 'N/A';
- let statusHtml = 'Unknown';
- let tooltip = `Device: ${deviceName}\nSerial/Ident: ${ident}`;
+ let status = 'Unknown';
+ let tip = `Device: ${name}\nSerial: ${ident}\n\nKey attributes:`;
- try {
- const state = dev.state || {};
- const health = state.smart_status?.passed ?? null;
-
- if (health !== null) {
- const icon = health
- ? ''
- : '';
- statusHtml = `${icon} ${health ? 'OK' : 'FAILED'}`;
- }
-
- const infoBody = JSON.stringify({
- type: 'A',
- device: deviceName
- });
-
- const infoResp = await this.ajaxCall(`/api/smart/service/info`, infoBody, 'POST');
-
- if (infoResp?.output && typeof infoResp.output === 'string' && infoResp.output.trim()) {
- const attrs = this.parseSmartAttributes(infoResp.output);
-
- tooltip += '\n\nKey Attributes:';
- if (attrs['194']) tooltip += `\nTemperature: ${attrs['194'].raw} °C`;
- if (attrs['9']) tooltip += `\nPower-On Hours: ${attrs['9'].raw} h`;
- if (attrs['5']) tooltip += `\nReallocated Sectors: ${attrs['5'].raw}`;
- if (attrs['197']) tooltip += `\nCurrent Pending: ${attrs['197'].raw}`;
- if (attrs['198']) tooltip += `\nOffline Uncorrectable: ${attrs['198'].raw}`;
- if (attrs['199']) tooltip += `\nUDMA CRC Errors: ${attrs['199'].raw}`;
- } else {
- tooltip += '\n\n(No detailed attributes available)';
- }
-
- } catch (e) {
- tooltip += `\n\nError fetching details: ${e.message || 'Unknown'}`;
+ const health = dev.state?.smart_status?.passed;
+ if (health !== undefined) {
+ const icon = health ? 'check-circle text-success' : 'exclamation-circle text-danger';
+ status = ` ${health ? 'OK' : 'FAILED'}`;
}
- const escapedTooltip = tooltip.replace(/"/g, '"').replace(/\n/g, '
');
- const deviceHtml = `${deviceName}`;
+ const resp = await this.ajaxCall('/api/smart/service/info', JSON.stringify({device: name, type: 'a', json: '1'}), 'POST');
+ const output = resp?.output;
- rows.push([deviceHtml, statusHtml]);
- }
+ if (output) {
+ tip += `\nTemp: ${output.temperature?.current ?? 'N/A'} °C`;
+ tip += `\nPower-On: ${output.power_on_time?.hours ?? 'N/A'} h`;
+ tip += `\nReallocated: ${this.getAttrRaw(output, 5)}`;
+ tip += `\nPending: ${this.getAttrRaw(output, 197)}`;
+ tip += `\nUncorrectable: ${this.getAttrRaw(output, 198)}`;
+ tip += `\nCRC Errors: ${this.getAttrRaw(output, 199)}`;
+ } else {
+ tip += '\n(No details available)';
+ }
- rows.sort((a, b) => String(a[0]).localeCompare(String(b[0])));
- super.updateTable('smart-table', rows);
+ const escaped = tip.replace(/"/g, '"').replace(/\n/g, '
');
+ return [`${name}`, status];
+ }));
- $('[data-toggle="tooltip"]').tooltip({container: 'body', html: true});
-
- } catch (err) {
- super.updateTable('smart-table', [[
- 'Widget error',
- `${err.message || 'Unknown'}`
- ]]);
+ super.updateTable('smart-table', rows.sort((a,b) => a[0].localeCompare(b[0])));
+ $('[data-toggle="tooltip"]').tooltip({container: 'body'});
+ } catch {
+ super.updateTable('smart-table', [['Error', 'Widget failed']]);
}
}
}