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 = $('
'); + + const headers = [ + this.translations?.device || 'Device', + this.translations?.status || 'SMART Status' + ]; + const $smarttable = this.createTable('smart-table', { - headerPosition: 'left', + headers: headers }); + + $smarttable.addClass('table table-striped table-condensed table-hover'); + $container.append($smarttable); return $container; } - async onWidgetTick() { - let disks = await this.ajaxCall(`/api/smart/service/${'list/detailed'}`, {}, 'POST'); - if (disks && disks.devices) { - const rows = []; - for (const device of disks.devices) { - try { - const health = device.state.smart_status.passed; - const text = health ? "OK" : "FAILED"; - const css = { color: health ? "green" : "red", fontSize: '150%' }; - const field = $(``).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']]); } } }