├── screenshots └── 01.jpg ├── root └── usr │ └── share │ ├── luci │ └── menu.d │ │ └── luci-app-disks-info.json │ └── rpcd │ ├── acl.d │ └── luci-app-disks-info.json │ └── ucode │ └── luci.disks-info ├── Makefile ├── README.md ├── LICENSE ├── po ├── templates │ └── disks-info.pot ├── ru │ └── disks-info.po └── zh_Hans │ └── disks-info.po └── htdocs └── luci-static └── resources └── view └── disks-info.js /screenshots/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gSpotx2f/luci-app-disks-info/HEAD/screenshots/01.jpg -------------------------------------------------------------------------------- /root/usr/share/luci/menu.d/luci-app-disks-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/status/disks-info": { 3 | "title": "Disk Devices", 4 | "order": 90, 5 | "action": { 6 | "type": "view", 7 | "path": "disks-info" 8 | }, 9 | "depends": { 10 | "acl": [ "luci-app-disks-info" ], 11 | "fs": { 12 | "/bin/df": "executable", 13 | "/usr/sbin/fdisk": "executable", 14 | "/usr/sbin/smartctl": "executable" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /root/usr/share/rpcd/acl.d/luci-app-disks-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-disks-info": { 3 | "description": "Grant access to disks-info procedures", 4 | "read": { 5 | "ubus": { 6 | "luci.disks-info": [ "getDevices" ] 7 | }, 8 | "cgi-io": [ "exec" ], 9 | "file": { 10 | "/usr/sbin/smartctl -d* -iAHl scttemp -l error -l devstat --json=c /dev/*": [ "exec" ], 11 | "/usr/sbin/smartctl -l scttempint,[0-9,p]* /dev/*": [ "exec" ] 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2025 gSpot (https://github.com/gSpotx2f/luci-app-disks-info) 3 | # 4 | # This is free software, licensed under the MIT License. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | PKG_NAME:=luci-app-disks-info 10 | PKG_VERSION:=0.6.0 11 | PKG_RELEASE:=1 12 | LUCI_TITLE:=Information about connected disk devices (partitions, filesystems, SMART). 13 | LUCI_DEPENDS:=+ucode +ucode-mod-fs +fdisk +smartmontools +smartmontools-drivedb 14 | LUCI_PKGARCH:=all 15 | PKG_LICENSE:=MIT 16 | 17 | #include ../../luci.mk 18 | include $(TOPDIR)/feeds/luci/luci.mk 19 | 20 | # call BuildPackage - OpenWrt buildroot signature 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luci-app-disks-info 2 | Status of connected disk devices (partitions, filesystems, SMART) for LuCI (OpenWrt webUI). 3 | 4 | OpenWrt >= 21.02. 5 | 6 | **Dependences:** ucode, ucode-mod-fs, fdisk, smartmontools, smartmontools-drivedb. 7 | 8 | ## Installation notes 9 | 10 | opkg update 11 | wget --no-check-certificate -O /tmp/luci-app-disks-info_0.6.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-disks-info_0.6.0-r1_all.ipk 12 | opkg install /tmp/luci-app-disks-info_0.6.0-r1_all.ipk 13 | rm /tmp/luci-app-disks-info_0.6.0-r1_all.ipk 14 | service rpcd restart 15 | 16 | i18n-ru: 17 | 18 | wget --no-check-certificate -O /tmp/luci-i18n-disks-info-ru_0.6.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-disks-info-ru_0.6.0-r1_all.ipk 19 | opkg install /tmp/luci-i18n-disks-info-ru_0.6.0-r1_all.ipk 20 | rm /tmp/luci-i18n-disks-info-ru_0.6.0-r1_all.ipk 21 | 22 | ## Screenshots: 23 | 24 | ![](https://github.com/gSpotx2f/luci-app-disks-info/blob/master/screenshots/01.jpg) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 gSpotx2f 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /root/usr/share/rpcd/ucode/luci.disks-info: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | import { lsdir, popen } from 'fs'; 5 | 6 | const fdiskPattern = '/usr/sbin/fdisk -lo Device,Start,End,Sectors,Size,Type %s 2> /dev/null'; 7 | const dfPattern = '/bin/df -Th %s 2> /dev/null'; 8 | const deviceRegexp = /^((h|s)d[a-z]|nvme[0-9]+n[0-9]+)$/; 9 | const devicesDir = '/dev'; 10 | 11 | function getCmdOutput(cmd) { 12 | let ret = ''; 13 | const fp = popen(cmd, 'r'); 14 | if(fp) { 15 | let c = fp.read('all'); 16 | fp.close(); 17 | if(c) { 18 | ret = trim(c); 19 | } 20 | } 21 | return ret; 22 | } 23 | 24 | function getDiskDevices() { 25 | let devices = lsdir(devicesDir); 26 | if(devices) { 27 | devices = map( 28 | filter(devices, e => match(e, deviceRegexp)), 29 | e => devicesDir + '/' + e 30 | ); 31 | } 32 | return devices ?? []; 33 | } 34 | 35 | function getFdiskData(device) { 36 | let diskDict = {}; 37 | let diskInfo = []; 38 | let partitions = []; 39 | let output = getCmdOutput(sprintf(fdiskPattern, device)); 40 | let diskData = map( 41 | split(trim(output), '\n\n', 3), 42 | e => trim(e) 43 | ); 44 | if(length(diskData) > 0) { 45 | diskInfo = map( 46 | map(split(diskData[0], '\n'), e => trim(e)), 47 | e => map(split(e, ':', 2), e => trim(e)) 48 | ); 49 | if(diskData[1]) { 50 | let partitionsRaw = map(split(diskData[1], '\n'), e => split(e, /\s+/)); 51 | for(let i = 1; i < length(partitionsRaw); i++) { 52 | let device, boot = false, start, end, sectors, size; 53 | let lastField = 5; 54 | if(partitionsRaw[i][1] == '*') { 55 | lastField = 6; 56 | device = partitionsRaw[i][0]; 57 | boot = true; 58 | start = partitionsRaw[i][2]; 59 | end = partitionsRaw[i][3]; 60 | sectors = partitionsRaw[i][4]; 61 | size = partitionsRaw[i][5]; 62 | } else { 63 | device = partitionsRaw[i][0]; 64 | start = partitionsRaw[i][1]; 65 | end = partitionsRaw[i][2]; 66 | sectors = partitionsRaw[i][3]; 67 | size = partitionsRaw[i][4]; 68 | } 69 | let type = []; 70 | for(let j = lastField; j < length(partitionsRaw[i]); j++) { 71 | push(type, partitionsRaw[i][j]); 72 | } 73 | push(partitions, { device, boot, start, end, sectors, size, type: join(' ', type) }); 74 | } 75 | } 76 | } 77 | diskDict['diskInfo'] = diskInfo; 78 | diskDict['partitions'] = partitions; 79 | return diskDict; 80 | } 81 | 82 | function getDfData(partition) { 83 | let fsDict = {}; 84 | let output = getCmdOutput(sprintf(dfPattern, partition)); 85 | let fsData = map( 86 | split(trim(output), '\n'), 87 | e => trim(e) 88 | ); 89 | if(fsData[1]) { 90 | let dataRaw = split(fsData[1], /\s+/); 91 | fsDict['filesystem'] = dataRaw[0]; 92 | fsDict['type'] = dataRaw[1]; 93 | fsDict['size'] = dataRaw[2]; 94 | fsDict['used'] = dataRaw[3]; 95 | fsDict['available'] = dataRaw[4]; 96 | fsDict['use_perc'] = dataRaw[5]; 97 | fsDict['mounted'] = dataRaw[6]; 98 | } 99 | return fsDict; 100 | } 101 | 102 | const methods = { 103 | getDevices: { 104 | call: function() { 105 | let data = {}; 106 | const devices = getDiskDevices(); 107 | if(length(devices) > 0) { 108 | let fdiskData = {}; 109 | for(let i in devices) { 110 | fdiskData[i] = getFdiskData(i); 111 | } 112 | let dfData = {}; 113 | for(let k, v in fdiskData) { 114 | let filesystems = []; 115 | for(let j in v.partitions) { 116 | let res = getDfData(j.device); 117 | if(length(res) > 0) { 118 | push(filesystems, res); 119 | } 120 | } 121 | dfData[k] = filesystems; 122 | } 123 | data['devices'] = () => { 124 | let d = []; 125 | for(let k in fdiskData) { 126 | push(d, k); 127 | } 128 | return d; 129 | }(); 130 | data['fdisk'] = fdiskData; 131 | data['df'] = dfData; 132 | } 133 | return data; 134 | } 135 | } 136 | }; 137 | 138 | return { 'luci.disks-info': methods }; 139 | -------------------------------------------------------------------------------- /po/templates/disks-info.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "Content-Type: text/plain; charset=UTF-8" 3 | 4 | msgid "ATA Version is" 5 | msgstr "" 6 | 7 | msgid "Apply" 8 | msgstr "" 9 | 10 | msgid "Attribute name" 11 | msgstr "" 12 | 13 | msgid "Available" 14 | msgstr "" 15 | 16 | msgid "Boot" 17 | msgstr "" 18 | 19 | msgid "Collecting data..." 20 | msgstr "" 21 | 22 | msgid "Current" 23 | msgstr "" 24 | 25 | msgid "Description" 26 | msgstr "" 27 | 28 | msgid "Device" 29 | msgstr "" 30 | 31 | msgid "Device Model" 32 | msgstr "" 33 | 34 | msgid "Device is" 35 | msgstr "" 36 | 37 | msgid "Device statistics" 38 | msgstr "" 39 | 40 | msgid "Device type" 41 | msgstr "" 42 | 43 | msgid "Disk model" 44 | msgstr "" 45 | 46 | msgid "Disk identifier" 47 | msgstr "" 48 | 49 | msgid "Disklabel type" 50 | msgstr "" 51 | 52 | msgid "Disk Devices" 53 | msgstr "" 54 | 55 | msgid "End" 56 | msgstr "" 57 | 58 | msgid "Error number" 59 | msgstr "" 60 | 61 | msgid "Estimated time" 62 | msgstr "" 63 | 64 | msgid "Filesystem" 65 | msgstr "" 66 | 67 | msgid "Firmware Version" 68 | msgstr "" 69 | 70 | msgid "Form Factor" 71 | msgstr "" 72 | 73 | msgid "Free-Fall Statistics" 74 | msgstr "" 75 | 76 | msgid "Gb" 77 | msgstr "" 78 | 79 | msgid "General Statistics" 80 | msgstr "" 81 | 82 | msgid "General Errors Statistics" 83 | msgstr "" 84 | 85 | msgid "Id" 86 | msgstr "" 87 | 88 | msgid "In smartctl database [for details use: -P show]" 89 | msgstr "" 90 | 91 | msgid "Index" 92 | msgstr "" 93 | 94 | msgid "LU WWN Device Id" 95 | msgstr "" 96 | 97 | msgid "Lifetime hours" 98 | msgstr "" 99 | 100 | msgid "Lifetime max" 101 | msgstr "" 102 | 103 | msgid "Lifetime min" 104 | msgstr "" 105 | 106 | msgid "Limit max" 107 | msgstr "" 108 | 109 | msgid "Limit min" 110 | msgstr "" 111 | 112 | msgid "Local Time is" 113 | msgstr "" 114 | 115 | msgid "May not be supported by some devices..." 116 | msgstr "" 117 | 118 | msgid "Model Family" 119 | msgstr "" 120 | 121 | msgid "Mounted filesystems" 122 | msgstr "" 123 | 124 | msgid "Mounted on" 125 | msgstr "" 126 | 127 | msgid "No devices detected" 128 | msgstr "" 129 | 130 | msgid "No mounted file systems" 131 | msgstr "" 132 | 133 | msgid "No partitions available" 134 | msgstr "" 135 | 136 | msgid "Not in smartctl database [for details use: -P showall]" 137 | msgstr "" 138 | 139 | msgid "Partitions" 140 | msgstr "" 141 | 142 | msgid "Percentage Used Endurance Indicator" 143 | msgstr "" 144 | 145 | msgid "Preserve across power cycles" 146 | msgstr "" 147 | 148 | msgid "RAW" 149 | msgstr "" 150 | 151 | msgid "Recommended max" 152 | msgstr "" 153 | 154 | msgid "Recommended min" 155 | msgstr "" 156 | 157 | msgid "Refresh devices" 158 | msgstr "" 159 | 160 | msgid "Rotating Media Statistics" 161 | msgstr "" 162 | 163 | msgid "Rotation Rate" 164 | msgstr "" 165 | 166 | msgid "S.M.A.R.T." 167 | msgstr "" 168 | 169 | msgid "S.M.A.R.T. error log" 170 | msgstr "" 171 | 172 | msgid "SATA Version is" 173 | msgstr "" 174 | 175 | msgid "SCT temperature history" 176 | msgstr "" 177 | 178 | msgid "SMART overall-health self-assessment test result:" 179 | msgstr "" 180 | 181 | msgid "Set logging interval" 182 | msgstr "" 183 | 184 | msgid "Sector Size" 185 | msgstr "" 186 | 187 | msgid "Sector size (logical/physical)" 188 | msgstr "" 189 | 190 | msgid "Sectors" 191 | msgstr "" 192 | 193 | msgid "Serial Number" 194 | msgstr "" 195 | 196 | msgid "Size" 197 | msgstr "" 198 | 199 | msgid "Solid State Device Statistics" 200 | msgstr "" 201 | 202 | msgid "Start" 203 | msgstr "" 204 | 205 | msgid "Status of connected disk devices." 206 | msgstr "" 207 | 208 | msgid "Temperature" 209 | msgstr "" 210 | 211 | msgid "Temperature Statistics" 212 | msgstr "" 213 | 214 | msgid "THRESH" 215 | msgstr "" 216 | 217 | msgid "Transport Statistics" 218 | msgstr "" 219 | 220 | msgid "Type" 221 | msgstr "" 222 | 223 | msgid "Units" 224 | msgstr "" 225 | 226 | msgid "Use" 227 | msgstr "" 228 | 229 | msgid "Used" 230 | msgstr "" 231 | 232 | msgid "User Capacity" 233 | msgstr "" 234 | 235 | msgid "VALUE" 236 | msgstr "" 237 | 238 | msgid "WHEN FAILED" 239 | msgstr "" 240 | 241 | msgid "WORST" 242 | msgstr "" 243 | 244 | msgid "Write to disk device memory" 245 | msgstr "" 246 | 247 | msgid "auto" 248 | msgstr "" 249 | 250 | msgid "blocks" 251 | msgstr "" 252 | 253 | msgid "bytes" 254 | msgstr "" 255 | 256 | msgid "failed" 257 | msgstr "" 258 | 259 | msgid "interval" 260 | msgstr "" 261 | 262 | msgid "logical/physical" 263 | msgstr "" 264 | 265 | msgid "min" 266 | msgstr "" 267 | 268 | msgid "no" 269 | msgstr "" 270 | 271 | msgid "passed" 272 | msgstr "" 273 | 274 | msgid "yes" 275 | msgstr "" 276 | -------------------------------------------------------------------------------- /po/ru/disks-info.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Content-Type: text/plain; charset=UTF-8\n" 4 | "Project-Id-Version: \n" 5 | "POT-Creation-Date: \n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: \n" 8 | "Language-Team: \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "Language: ru\n" 12 | "X-Generator: Poedit 2.0.6\n" 13 | 14 | msgid "ATA Version is" 15 | msgstr "Версия ATA" 16 | 17 | msgid "Apply" 18 | msgstr "Применить" 19 | 20 | msgid "Attribute name" 21 | msgstr "Имя атрибута" 22 | 23 | msgid "Available" 24 | msgstr "Доступно" 25 | 26 | msgid "Boot" 27 | msgstr "Загрузочный" 28 | 29 | msgid "Collecting data..." 30 | msgstr "Сбор данных..." 31 | 32 | msgid "Current" 33 | msgstr "Текущая" 34 | 35 | msgid "Description" 36 | msgstr "Описание" 37 | 38 | msgid "Device" 39 | msgstr "Устройство" 40 | 41 | msgid "Device Model" 42 | msgstr "Модель устройства" 43 | 44 | msgid "Device is" 45 | msgstr "Устройство" 46 | 47 | msgid "Device statistics" 48 | msgstr "Статистика устройства" 49 | 50 | msgid "Device type" 51 | msgstr "Тип устройства" 52 | 53 | msgid "Disk model" 54 | msgstr "Модель диска" 55 | 56 | msgid "Disk identifier" 57 | msgstr "Идентификатор диска" 58 | 59 | msgid "Disklabel type" 60 | msgstr "Тип разметки диска" 61 | 62 | msgid "Disk Devices" 63 | msgstr "Дисковые устройства" 64 | 65 | msgid "End" 66 | msgstr "Конец" 67 | 68 | msgid "Error number" 69 | msgstr "Номер ошибки" 70 | 71 | msgid "Estimated time" 72 | msgstr "Расчётное время" 73 | 74 | msgid "Filesystem" 75 | msgstr "Файловая система" 76 | 77 | msgid "Firmware Version" 78 | msgstr "Версия прошивки" 79 | 80 | msgid "Form Factor" 81 | msgstr "Форм-фактор" 82 | 83 | msgid "Free-Fall Statistics" 84 | msgstr "Статистика падений" 85 | 86 | msgid "Gb" 87 | msgstr "Гб" 88 | 89 | msgid "General Statistics" 90 | msgstr "Общая статистика" 91 | 92 | msgid "General Errors Statistics" 93 | msgstr "Общая статистика ошибок" 94 | 95 | msgid "Id" 96 | msgstr "" 97 | 98 | msgid "In smartctl database [for details use: -P show]" 99 | msgstr "Находится в базе smartctl [подробно: -P show]" 100 | 101 | msgid "Index" 102 | msgstr "Индекс" 103 | 104 | msgid "LU WWN Device Id" 105 | msgstr "" 106 | 107 | msgid "Lifetime hours" 108 | msgstr "Часов наработки" 109 | 110 | msgid "Lifetime max" 111 | msgstr "Максимальная за время работы" 112 | 113 | msgid "Lifetime min" 114 | msgstr "Минимальная за время работы" 115 | 116 | msgid "Limit max" 117 | msgstr "Лимит макс" 118 | 119 | msgid "Limit min" 120 | msgstr "Лимит мин" 121 | 122 | msgid "Local Time is" 123 | msgstr "Локальное время" 124 | 125 | msgid "May not be supported by some devices..." 126 | msgstr "Может не поддерживаться некоторыми устройствами..." 127 | 128 | msgid "Model Family" 129 | msgstr "Семейство моделей" 130 | 131 | msgid "Mounted filesystems" 132 | msgstr "Смонтированные файловые системы" 133 | 134 | msgid "Mounted on" 135 | msgstr "Смонтировано" 136 | 137 | msgid "No devices detected" 138 | msgstr "Устройства не найдены" 139 | 140 | msgid "No mounted filesystems" 141 | msgstr "Нет смонтированных файловых систем" 142 | 143 | msgid "No partitions available" 144 | msgstr "Нет доступных разделов" 145 | 146 | msgid "Not in smartctl database [for details use: -P showall]" 147 | msgstr "Отсутствует в базе smartctl [подробно: -P showall]" 148 | 149 | msgid "Partitions" 150 | msgstr "Разделы" 151 | 152 | msgid "Percentage Used Endurance Indicator" 153 | msgstr "Процентный индикатор износа" 154 | 155 | msgid "Preserve across power cycles" 156 | msgstr "Сохранить при выключении питания" 157 | 158 | msgid "RAW" 159 | msgstr "" 160 | 161 | msgid "Recommended max" 162 | msgstr "Рекомендованная макс" 163 | 164 | msgid "Recommended min" 165 | msgstr "Рекомендованная мин" 166 | 167 | msgid "Refresh devices" 168 | msgstr "Обновить устройства" 169 | 170 | msgid "Rotating Media Statistics" 171 | msgstr "Статистика механической части" 172 | 173 | msgid "Rotation Rate" 174 | msgstr "Скорость вращения" 175 | 176 | msgid "S.M.A.R.T." 177 | msgstr "" 178 | 179 | msgid "S.M.A.R.T. error log" 180 | msgstr "Лог ошибок S.M.A.R.T." 181 | 182 | msgid "SATA Version is" 183 | msgstr "Версия SATA" 184 | 185 | msgid "SCT temperature history" 186 | msgstr "Сводка температуры SCT" 187 | 188 | msgid "SMART overall-health self-assessment test result:" 189 | msgstr "Результат теста оценки общего состояния SMART:" 190 | 191 | msgid "Set logging interval" 192 | msgstr "Интервал логирования" 193 | 194 | msgid "Sector Size" 195 | msgstr "Размер сектора" 196 | 197 | msgid "Sector size (logical/physical)" 198 | msgstr "Размер сектора (логический/физический)" 199 | 200 | msgid "Sectors" 201 | msgstr "Секторы" 202 | 203 | msgid "Serial Number" 204 | msgstr "Серийный номер" 205 | 206 | msgid "Size" 207 | msgstr "Размер" 208 | 209 | msgid "Solid State Device Statistics" 210 | msgstr "Статистика SSD" 211 | 212 | msgid "Start" 213 | msgstr "Начало" 214 | 215 | msgid "Status of connected disk devices." 216 | msgstr "Состояние подключенных дисковых устройств." 217 | 218 | msgid "Temperature" 219 | msgstr "Температура" 220 | 221 | msgid "Temperature Statistics" 222 | msgstr "Статистика температуры" 223 | 224 | msgid "THRESH" 225 | msgstr "" 226 | 227 | msgid "Transport Statistics" 228 | msgstr "Статистика транспорта" 229 | 230 | msgid "Type" 231 | msgstr "Тип" 232 | 233 | msgid "Units" 234 | msgstr "Единицы объёма" 235 | 236 | msgid "Use" 237 | msgstr "Используется" 238 | 239 | msgid "Used" 240 | msgstr "Использовано" 241 | 242 | msgid "User Capacity" 243 | msgstr "Доступная ёмкость" 244 | 245 | msgid "VALUE" 246 | msgstr "" 247 | 248 | msgid "WHEN FAILED" 249 | msgstr "" 250 | 251 | msgid "WORST" 252 | msgstr "" 253 | 254 | msgid "Write to disk device memory" 255 | msgstr "Записать в память дискового устройства" 256 | 257 | msgid "auto" 258 | msgstr "авто" 259 | 260 | msgid "blocks" 261 | msgstr "блоков" 262 | 263 | msgid "bytes" 264 | msgstr "байт" 265 | 266 | msgid "failed" 267 | msgstr "не пройден" 268 | 269 | msgid "interval" 270 | msgstr "интервал" 271 | 272 | msgid "logical/physical" 273 | msgstr "логический/физический" 274 | 275 | msgid "min" 276 | msgstr "мин" 277 | 278 | msgid "no" 279 | msgstr "нет" 280 | 281 | msgid "passed" 282 | msgstr "пройден" 283 | 284 | msgid "да" 285 | msgstr "" 286 | -------------------------------------------------------------------------------- /po/zh_Hans/disks-info.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Content-Type: text/plain; charset=UTF-8\n" 4 | "Project-Id-Version: \n" 5 | "POT-Creation-Date: \n" 6 | "PO-Revision-Date: \n" 7 | "Last-Translator: Frand.Ren \n" 8 | "Language-Team: Chinese (Simplified) \n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Language: zh_Hans\n" 13 | "X-Generator: Weblate 4.6-dev\n" 14 | 15 | msgid "ATA Version is" 16 | msgstr "ATA 版本" 17 | 18 | msgid "Apply" 19 | msgstr "应用" 20 | 21 | msgid "Attribute name" 22 | msgstr "参数名" 23 | 24 | msgid "Available" 25 | msgstr "可用" 26 | 27 | msgid "Boot" 28 | msgstr "引导" 29 | 30 | msgid "Collecting data..." 31 | msgstr "正在收集数据..." 32 | 33 | msgid "Current" 34 | msgstr "当前" 35 | 36 | msgid "Description" 37 | msgstr "描述" 38 | 39 | msgid "Device" 40 | msgstr "设备" 41 | 42 | msgid "Device is" 43 | msgstr "设备" 44 | 45 | msgid "Device Model" 46 | msgstr "设备型号" 47 | 48 | msgid "Device statistics" 49 | msgstr "设备统计信息" 50 | 51 | msgid "Disk model" 52 | msgstr "型号" 53 | 54 | msgid "Disk identifier" 55 | msgstr "磁盘标识符" 56 | 57 | msgid "Disklabel type" 58 | msgstr "分区表类型" 59 | 60 | msgid "Disk Devices" 61 | msgstr "磁盘设备" 62 | 63 | msgid "End" 64 | msgstr "结束" 65 | 66 | msgid "Error number" 67 | msgstr "错误号" 68 | 69 | msgid "Estimated time" 70 | msgstr "预计时间" 71 | 72 | msgid "Filesystems" 73 | msgstr "文件系统" 74 | 75 | msgid "Filesystem" 76 | msgstr "文件系统" 77 | 78 | msgid "Firmware Version" 79 | msgstr "固件版本" 80 | 81 | msgid "Form Factor" 82 | msgstr "外形" 83 | 84 | msgid "Free-Fall Statistics" 85 | msgstr "自由跌落统计" 86 | 87 | msgid "Gb" 88 | msgstr "" 89 | 90 | msgid "General Statistics" 91 | msgstr "常见信息统计" 92 | 93 | msgid "General Errors Statistics" 94 | msgstr "错误信息统计" 95 | 96 | msgid "Id" 97 | msgstr "ID" 98 | 99 | msgid "In smartctl database [for details use: -P show]" 100 | msgstr "在 smartctl 数据库中 [详细使用说明:-P show]" 101 | 102 | msgid "Index" 103 | msgstr "索引" 104 | 105 | msgid "Information about the connected disk devices." 106 | msgstr "有关已连接磁盘设备的信息" 107 | 108 | msgid "LU WWN Device Id" 109 | msgstr "LI WWN 设备ID" 110 | 111 | msgid "Lifetime hours" 112 | msgstr "生命小时数" 113 | 114 | msgid "Lifetime max" 115 | msgstr "历史最高温" 116 | 117 | msgid "Lifetime min" 118 | msgstr "历史最低温" 119 | 120 | msgid "Limit max" 121 | msgstr "上限温度" 122 | 123 | msgid "Limit min" 124 | msgstr "下限温度" 125 | 126 | msgid "Local Time is" 127 | msgstr "当前时间" 128 | 129 | msgid "May not be supported by some devices..." 130 | msgstr "某些设备可能不支持..." 131 | 132 | msgid "Model Family" 133 | msgstr "家族型号" 134 | 135 | msgid "Mounted on" 136 | msgstr "挂载点" 137 | 138 | msgid "No devices detected" 139 | msgstr "未检测到设备" 140 | 141 | msgid "No mounted filesystems" 142 | msgstr "没有挂载文件系统" 143 | 144 | msgid "No partitions available" 145 | msgstr "没有可用分区" 146 | 147 | msgid "Not in smartctl database [for details use: -P showall]" 148 | msgstr "不在 smartctl 数据库中 [详细使用说明:-P showall]" 149 | 150 | msgid "Partitions" 151 | msgstr "分区" 152 | 153 | msgid "Percentage Used Endurance Indicator" 154 | msgstr "已使用寿命" 155 | 156 | msgid "Preserve across power cycles" 157 | msgstr "重启后不变" 158 | 159 | msgid "RAW" 160 | msgstr "原始值" 161 | 162 | msgid "Recommended max" 163 | msgstr "推荐最高温度" 164 | 165 | msgid "Recommended min" 166 | msgstr "推荐最低温度" 167 | 168 | msgid "Refresh devices" 169 | msgstr "重新加载设备信息" 170 | 171 | msgid "Rotating Media Statistics" 172 | msgstr "旋转媒体统计" 173 | 174 | msgid "Rotation Rate" 175 | msgstr "转速" 176 | 177 | msgid "S.M.A.R.T." 178 | msgstr "" 179 | 180 | msgid "S.M.A.R.T. error log" 181 | msgstr "S.M.A.R.T. 错误日志" 182 | 183 | msgid "SATA Version is" 184 | msgstr "SATA 版本" 185 | 186 | msgid "SCT temperature history" 187 | msgstr "SCT 温度历史" 188 | 189 | msgid "SMART overall-health self-assessment test result:" 190 | msgstr "SMART 自我评估:" 191 | 192 | msgid "Set logging interval" 193 | msgstr "设置记录间隔" 194 | 195 | msgid "Sector Size" 196 | msgstr "扇区大小" 197 | 198 | msgid "Sector size (logical/physical)" 199 | msgstr "扇区大小(逻辑/物理)" 200 | 201 | msgid "Sectors" 202 | msgstr "扇区" 203 | 204 | msgid "Serial Number" 205 | msgstr "序列号" 206 | 207 | msgid "Size" 208 | msgstr "大小" 209 | 210 | msgid "Solid State Device Statistics" 211 | msgstr "固态硬盘统计信息" 212 | 213 | msgid "Start" 214 | msgstr "开始" 215 | 216 | msgid "Temperature" 217 | msgstr "温度" 218 | 219 | msgid "Temperature Statistics" 220 | msgstr "温度统计" 221 | 222 | msgid "THRESH" 223 | msgstr "临界值" 224 | 225 | msgid "Transport Statistics" 226 | msgstr "传输统计" 227 | 228 | msgid "Type" 229 | msgstr "类型" 230 | 231 | msgid "Units" 232 | msgstr "单位" 233 | 234 | msgid "Use" 235 | msgstr "已使用" 236 | 237 | msgid "Used" 238 | msgstr "已使用" 239 | 240 | msgid "User Capacity" 241 | msgstr "总大小" 242 | 243 | msgid "VALUE" 244 | msgstr "当前值" 245 | 246 | msgid "WHEN FAILED" 247 | msgstr "失效时间" 248 | 249 | msgid "WORST" 250 | msgstr "最差值" 251 | 252 | msgid "Write to device memory" 253 | msgstr "保存配置" 254 | 255 | msgid "blocks" 256 | msgstr "块" 257 | 258 | msgid "bytes" 259 | msgstr "字节" 260 | 261 | msgid "failed" 262 | msgstr "失败" 263 | 264 | msgid "interval" 265 | msgstr "间隔" 266 | 267 | msgid "logical/physical" 268 | msgstr "逻辑/物理" 269 | 270 | msgid "min" 271 | msgstr "分钟" 272 | 273 | msgid "passed" 274 | msgstr "通过" 275 | 276 | msgid "I/O size (minimum/optimal)" 277 | msgstr "I/O 大小 (最小/最优)" 278 | 279 | msgid "Number of Hardware Resets" 280 | msgstr "硬件复位计数" 281 | 282 | msgid "Number of Interface CRC Errors" 283 | msgstr "接口CRC错误计数" 284 | 285 | msgid "Lifetime Power-On Resets" 286 | msgstr "上电复位计数" 287 | 288 | msgid "Power-on Hours" 289 | msgstr "通电小时" 290 | 291 | msgid "Logical Sectors Written" 292 | msgstr "逻辑扇区写计数" 293 | 294 | msgid "Number of Write Commands" 295 | msgstr "命令写计数" 296 | 297 | msgid "Logical Sectors Read" 298 | msgstr "逻辑扇区读计数" 299 | 300 | msgid "Number of Read Commands" 301 | msgstr "命令读计数" 302 | 303 | msgid "Number of Reported Uncorrectable Errors" 304 | msgstr "无法修复的错误计数" 305 | 306 | msgid "Resets Between Cmd Acceptance and Completion" 307 | msgstr "命令接收与完成之间的复位计数" 308 | 309 | msgid "Current Temperature" 310 | msgstr "当前温度" 311 | 312 | msgid "Average Short Term Temperature" 313 | msgstr "短期平均温度" 314 | 315 | msgid "Average Long Term Temperature" 316 | msgstr "长期平均温度" 317 | 318 | msgid "Highest Temperature" 319 | msgstr "最高温度" 320 | 321 | msgid "Lowest Temperature" 322 | msgstr "最低温度" 323 | 324 | msgid "Highest Average Short Term Temperature" 325 | msgstr "短期最高平均温度" 326 | 327 | msgid "Lowest Average Short Term Temperature" 328 | msgstr "短期最低平均温度" 329 | 330 | msgid "Highest Average Long Term Temperature" 331 | msgstr "长期最高平均温度" 332 | 333 | msgid "Lowest Average Long Term Temperature" 334 | msgstr "长期最低平均温度" 335 | 336 | msgid "Time in Over-Temperature" 337 | msgstr "超温时间" 338 | 339 | msgid "Specified Maximum Operating Temperature" 340 | msgstr "推荐最高工作温度" 341 | 342 | msgid "Time in Under-Temperature" 343 | msgstr "低温时间" 344 | 345 | msgid "Specified Minimum Operating Temperature" 346 | msgstr "推荐最低工作温度" 347 | 348 | msgid "Solid State Device" 349 | msgstr "固态硬盘" 350 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/disks-info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require fs'; 3 | 'require rpc'; 4 | 'require ui'; 5 | 'require view'; 6 | 7 | document.head.append(E('style', {'type': 'text/css'}, 8 | ` 9 | :root { 10 | --app-disks-info-dark-font-color: #2e2e2e; 11 | --app-disks-info-light-font-color: #fff; 12 | --app-disks-info-warn-color: #fff7e2; 13 | --app-disks-info-err-color: #fcc3bf; 14 | --app-disks-info-ok-color-label: #2ea256; 15 | --app-disks-info-err-color-label: #ff4e54; 16 | } 17 | :root[data-darkmode="true"] { 18 | --app-disks-info-dark-font-color: #fff; 19 | --app-disks-info-light-font-color: #fff; 20 | --app-disks-info-warn-color: #8d7000; 21 | --app-disks-info-err-color: #a93734; 22 | --app-disks-info-ok-color-label: #007627; 23 | --app-disks-info-err-color-label: #a93734; 24 | } 25 | .disks-info-label-status { 26 | display: inline; 27 | margin: 0 4px !important; 28 | padding: 1px 4px; 29 | -webkit-border-radius: 3px; 30 | -moz-border-radius: 3px; 31 | border-radius: 3px; 32 | text-transform: uppercase; 33 | font-weight: bold; 34 | line-height: 1.6em; 35 | } 36 | .disks-info-ok-label { 37 | background-color: var(--app-disks-info-ok-color-label) !important; 38 | color: var(--app-disks-info-light-font-color) !important; 39 | } 40 | .disks-info-err-label { 41 | background-color: var(--app-disks-info-err-color-label) !important; 42 | color: var(--app-disks-info-light-font-color) !important; 43 | } 44 | .disks-info-warn { 45 | background-color: var(--app-disks-info-warn-color) !important; 46 | color: var(--app-disks-info-dark-font-color) !important; 47 | } 48 | .disks-info-warn .td { 49 | color: var(--app-disks-info-dark-font-color) !important; 50 | } 51 | .disks-info-warn td { 52 | color: var(--app-disks-info-dark-font-color) !important; 53 | } 54 | .disks-info-err { 55 | background-color: var(--app-disks-info-err-color) !important; 56 | color: var(--app-disks-info-dark-font-color) !important; 57 | } 58 | .disks-info-err .td { 59 | color: var(--app-disks-info-dark-font-color) !important; 60 | } 61 | .disks-info-err td { 62 | color: var(--app-disks-info-dark-font-color) !important; 63 | } 64 | `)); 65 | 66 | return view.extend({ 67 | viewName : 'disks-info', 68 | 69 | fsSpaceWarning : 90, 70 | 71 | ssdEnduranceWarning : 95, 72 | 73 | diskTempWarningDefault : 60, 74 | 75 | diskTempCriticalDefault: 80, 76 | 77 | smartCriticalAttrs : [ 5, 11, 183, 184, 187, 196, 197, 198, 200, 202, 220 ], 78 | 79 | smartTempAttrs : [ 190, 194 ], 80 | 81 | deviceRegExp : new RegExp('^((h|s)d[a-z]|nvme[0-9]+n[0-9]+)$'), 82 | 83 | availDeviceTypes : [ 84 | { name: 'auto', title: _('auto') }, 85 | { name: 'sat', title: _('SAT (SCSI to ATA Translation)') }, 86 | ], 87 | 88 | smartctl : '/usr/sbin/smartctl', 89 | 90 | deviceType : {}, 91 | 92 | devices : [], 93 | 94 | callDevices : rpc.declare({ 95 | object: 'luci.disks-info', 96 | method: 'getDevices', 97 | expect: { '': {} }, 98 | }), 99 | 100 | restoreSettingsFromLocalStorage() { 101 | let deviceType = localStorage.getItem(`luci-app-${this.viewName}-deviceType`); 102 | if(deviceType) { 103 | let items = deviceType.split(';'); 104 | if(items.length > 0) { 105 | for(let i of items) { 106 | let [k, v] = i.split('='); 107 | if(k && v) { 108 | this.deviceType[k] = v; 109 | }; 110 | }; 111 | }; 112 | }; 113 | }, 114 | 115 | saveSettingsToLocalStorage() { 116 | let items = []; 117 | for(let [k, v] of Object.entries(this.deviceType)) { 118 | items.push(`${k}=${v}`); 119 | }; 120 | localStorage.setItem( 121 | `luci-app-${this.viewName}-deviceType`, items.join(';')); 122 | }, 123 | 124 | getDeviceData(device) { 125 | let deviceType = this.deviceType[device] || this.availDeviceTypes[0].name; 126 | return Promise.all([ 127 | device, 128 | L.resolveDefault(fs.exec_direct( 129 | this.smartctl, 130 | [ '-d', deviceType, '-iAHl', 'scttemp', '-l', 'error', '-l', 'devstat', '--json=c', device ], 131 | 'json'), null), 132 | ]); 133 | }, 134 | 135 | setSctTempLogInterval(device) { 136 | let deviceNormalized = device.replace(/\//g, '-'); 137 | let num = document.getElementById('logging_interval_value' + deviceNormalized).value; 138 | let pSave = document.getElementById('logging_interval_type' + deviceNormalized).checked; 139 | 140 | if(/^[0-9]{1,2}$/.test(num) && Number(num) > 0) { 141 | num = String(Number(num)); 142 | } else { 143 | return; 144 | }; 145 | 146 | return fs.exec(this.smartctl, 147 | [ '-l', 'scttempint,' + (pSave ? num + ',p' : num), device ] 148 | ).then(res => { 149 | window.location.reload(); 150 | }).catch(e => ui.addNotification(null, E('p', {}, e.message))); 151 | }, 152 | 153 | createDiskTable(fdiskData, dfData) { 154 | let diskInfoTable = E('table', { 'class': 'table' }); 155 | for(let i of fdiskData.diskInfo) { 156 | diskInfoTable.append( 157 | E('tr', { 'class': 'tr' }, [ 158 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _(i[0]) + ':'), 159 | E('td', { 'class': 'td left' }, i[1]), 160 | ]) 161 | ); 162 | }; 163 | 164 | let partitionsTablePlaceholder = E('tr', { 'class': 'tr placeholder' }, 165 | E('td', { 'class': 'td' }, 166 | E('em', {}, _('No partitions available')) 167 | ) 168 | ); 169 | let dfTablePlaceholder = E('tr', { 'class': 'tr placeholder' }, 170 | E('td', { 'class': 'td' }, 171 | E('em', {}, _('No mounted filesystems')) 172 | ) 173 | ); 174 | let partitionsTableTitles = [ 175 | _('Device'), 176 | _('Boot'), 177 | _('Start'), 178 | _('End'), 179 | _('Sectors'), 180 | _('Size'), 181 | _('Type'), 182 | ]; 183 | let partitionsTable = E('table', { 'class': 'table' }, 184 | E('tr', { 'class': 'tr table-titles' }, [ 185 | E('th', { 'class': 'th left' }, partitionsTableTitles[0]), 186 | E('th', { 'class': 'th left' }, partitionsTableTitles[1]), 187 | E('th', { 'class': 'th left' }, partitionsTableTitles[2]), 188 | E('th', { 'class': 'th left' }, partitionsTableTitles[3]), 189 | E('th', { 'class': 'th left' }, partitionsTableTitles[4]), 190 | E('th', { 'class': 'th left' }, partitionsTableTitles[5]), 191 | E('th', { 'class': 'th left' }, partitionsTableTitles[6]), 192 | ]) 193 | ); 194 | let dfTableTitles = [ 195 | _('Filesystem'), 196 | _('Type'), 197 | _('Size'), 198 | _('Used'), 199 | _('Available'), 200 | _('Use') + ' %', 201 | _('Mounted on'), 202 | ]; 203 | let dfTable = E('table', { 'class': 'table' }, 204 | E('tr', { 'class': 'tr table-titles' }, [ 205 | E('th', { 'class': 'th left' }, dfTableTitles[0]), 206 | E('th', { 'class': 'th left' }, dfTableTitles[1]), 207 | E('th', { 'class': 'th left' }, dfTableTitles[2]), 208 | E('th', { 'class': 'th left' }, dfTableTitles[3]), 209 | E('th', { 'class': 'th left' }, dfTableTitles[4]), 210 | E('th', { 'class': 'th center' }, dfTableTitles[5]), 211 | E('th', { 'class': 'th left' }, dfTableTitles[6]), 212 | ]) 213 | ); 214 | 215 | let partitions = fdiskData.partitions; 216 | if(partitions) { 217 | for(let i of partitions) { 218 | partitionsTable.append( 219 | E('tr', { 'class': 'tr' }, [ 220 | E('td', { 221 | 'class' : 'td left', 222 | 'data-title': partitionsTableTitles[0], 223 | }, i.device), 224 | E('td', { 225 | 'class' : 'td left', 226 | 'data-title': partitionsTableTitles[1], 227 | }, (i.boot) ? _('yes') : _('no')), 228 | E('td', { 229 | 'class' : 'td left', 230 | 'data-title': partitionsTableTitles[2], 231 | }, i.start), 232 | E('td', { 233 | 'class' : 'td left', 234 | 'data-title': partitionsTableTitles[3], 235 | }, i.end), 236 | E('td', { 237 | 'class' : 'td left', 238 | 'data-title': partitionsTableTitles[4], 239 | }, i.sectors), 240 | E('td', { 241 | 'class' : 'td left', 242 | 'data-title': partitionsTableTitles[6], 243 | }, i.size), 244 | E('td', { 245 | 'class' : 'td left', 246 | 'data-title': partitionsTableTitles[7], 247 | }, i.type), 248 | ]) 249 | ); 250 | }; 251 | if(partitionsTable.children.length <= 1) { 252 | partitionsTable.append(partitionsTablePlaceholder); 253 | } else if(dfData) { 254 | for(let i of dfData) { 255 | dfTable.append( 256 | E('tr', { 'class': 'tr' }, [ 257 | E('td', { 258 | 'class' : 'td left', 259 | 'data-title': dfTableTitles[0], 260 | }, i.filesystem), 261 | E('td', { 262 | 'class' : 'td left', 263 | 'data-title': dfTableTitles[1], 264 | }, i.type), 265 | E('td', { 266 | 'class' : 'td left', 267 | 'data-title': dfTableTitles[2], 268 | }, i.size), 269 | E('td', { 270 | 'class' : 'td left', 271 | 'data-title': dfTableTitles[3], 272 | }, i.used), 273 | E('td', { 274 | 'class' : 'td left', 275 | 'data-title': dfTableTitles[4], 276 | }, i.available), 277 | E('td', { 278 | 'class': (parseInt(i.use_perc) >= this.fsSpaceWarning) ? 279 | 'td left disks-info-warn' : 'td left', 280 | 'data-title': dfTableTitles[5], 281 | }, E('div', { 282 | 'class': 'cbi-progressbar', 283 | 'title': i.use_perc, 284 | 'style': 'min-width:8em !important', 285 | }, 286 | E('div', { 'style': 'width:' + i.use_perc }) 287 | ) 288 | ), 289 | E('td', { 290 | 'class' : 'td left', 291 | 'data-title': dfTableTitles[6], 292 | }, i.mounted), 293 | ]), 294 | ); 295 | }; 296 | if(dfTable.children.length <= 1) { 297 | dfTable.append(dfTablePlaceholder); 298 | }; 299 | }; 300 | } else { 301 | partitionsTable.append(partitionsTablePlaceholder); 302 | dfTable.append(dfTablePlaceholder); 303 | }; 304 | 305 | return E([ 306 | E('div', { 'class': 'cbi-value' }, diskInfoTable), 307 | E('div', { 'class': 'cbi-value' }, [ 308 | E('h3', {}, _('Partitions') + ':'), 309 | partitionsTable, 310 | ]), 311 | E('div', { 'class': 'cbi-value' }, [ 312 | E('h3', {}, _('Mounted filesystems') + ':'), 313 | dfTable, 314 | ]), 315 | ]); 316 | }, 317 | 318 | createSmartTable(smartObject) { 319 | let smartStatusLabel = (smartObject.smart_status.passed) ? 320 | E('span', { 'class': 'disks-info-label-status disks-info-ok-label' }, 321 | _('passed')) 322 | : 323 | E('span', { 'class': 'disks-info-label-status disks-info-err-label' }, 324 | _('failed')); 325 | 326 | let smartStatus = E('h5', { 327 | 'style': 'width:100% !important; text-align:center !important', 328 | }, [ 329 | _('SMART overall-health self-assessment test result:'), 330 | smartStatusLabel, 331 | ]); 332 | 333 | let smartAttrsTable = E('table', { 'class': 'table' }, 334 | E('tr', { 'class': 'tr table-titles' }, [ 335 | E('th', { 'class': 'th right' }, _('Id')), 336 | E('th', { 'class': 'th left' }, _('Attribute name')), 337 | E('th', { 'class': 'th left' }, _('RAW')), 338 | E('th', { 'class': 'th left' }, _('VALUE')), 339 | E('th', { 'class': 'th left' }, _('WORST')), 340 | E('th', { 'class': 'th left' }, _('THRESH')), 341 | E('th', { 'class': 'th left' }, _('WHEN FAILED')), 342 | ]) 343 | ); 344 | 345 | for(let attr of smartObject.ata_smart_attributes.table) { 346 | let tempValue; 347 | let lineStyle = (attr.value <= attr.thresh) ? 'tr disks-info-err' : 348 | (this.smartCriticalAttrs.includes(attr.id) && attr.raw.value > 0) ? 'tr disks-info-warn' : 349 | (this.smartTempAttrs.includes(attr.id) && +(attr.raw.string.split(' ')[0]) >= this.diskTempWarning) ? 350 | 'tr disks-info-warn' : 'tr'; 351 | 352 | smartAttrsTable.append( 353 | E('tr', { 354 | 'class': lineStyle, 355 | }, [ 356 | E('td', { 'class': 'td right', 'data-title': _('Id') }, 357 | E('span', { 358 | 'style': 'cursor:help; border-bottom:1px dotted', 359 | 'data-tooltip': 'hex: %02X'.format(attr.id) 360 | }, attr.id) 361 | ), 362 | E('td', { 'class': 'td left', 'data-title': _('Attribute name') }, 363 | attr.name.replace(/_/g, ' ')), 364 | E('td', { 'class': 'td left', 'data-title': _('RAW') }, 365 | E('span', { 366 | 'style': 'cursor:help; border-bottom:1px dotted; font-weight:bold', 367 | 'data-tooltip': 'hex: %012X'.format(attr.raw.value) 368 | }, attr.raw.string) 369 | ), 370 | E('td', { 'class': 'td left', 'data-title': _('VALUE') }, 371 | '%03d'.format(attr.value)), 372 | E('td', { 'class': 'td left', 'data-title': _('WORST') }, 373 | '%03d'.format(attr.worst)), 374 | E('td', { 'class': 'td left', 'data-title': _('THRESH') }, 375 | '%03d'.format(attr.thresh)), 376 | E('td', { 'class': 'td left', 'data-title': _('WHEN FAILED') }, 377 | attr.when_failed || '-'), 378 | ]) 379 | ); 380 | }; 381 | 382 | return E('div', { 'class': 'cbi-value' }, [ 383 | E('h3', {}, _('S.M.A.R.T.') + ':'), 384 | smartStatus, 385 | smartAttrsTable, 386 | ]); 387 | }, 388 | 389 | createErrorLog(table) { 390 | let errorLogTable = E('table', { 'class': 'table' }, 391 | E('tr', { 'class': 'tr table-titles' }, [ 392 | E('th', { 'class': 'th left', 'style':'min-width:16%' }, _('Error number')), 393 | E('th', { 'class': 'th left', 'style':'min-width:17%' }, _('Lifetime hours')), 394 | E('th', { 'class': 'th left' }, _('Description')), 395 | ]) 396 | ); 397 | for(let errObj of table) { 398 | errorLogTable.append( 399 | E('tr', { 'class': 'tr' }, [ 400 | E('td', { 'class': 'td left', 'data-title': _('Error number') }, 401 | errObj.error_number), 402 | E('td', { 'class': 'td left', 'data-title': _('Lifetime hours') }, 403 | errObj.lifetime_hours), 404 | E('td', { 'class': 'td left', 'data-title': _('Description') }, 405 | errObj.error_description), 406 | ]) 407 | ); 408 | }; 409 | return E('div', { 'class': 'cbi-value' }, [ 410 | E('h3', {}, _('S.M.A.R.T. error log') + ':'), 411 | E('div', { 'style': 'width:100%; max-height:20em; overflow:auto' }, 412 | errorLogTable 413 | ), 414 | ]); 415 | }, 416 | 417 | createTempTable(smartObject) { 418 | return E('div', { 'class': 'cbi-value' }, [ 419 | E('h3', {}, _('Temperature') + ':'), 420 | E('table', { 'class': 'table' }, [ 421 | E('tr', { 422 | 'class': (smartObject.temperature.current >= smartObject.temperature.op_limit_max) ? 423 | 'tr disks-info-err' : (smartObject.temperature.current >= this.diskTempWarning) ? 424 | 'tr disks-info-warn' : 'tr', 425 | }, [ 426 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Current') + ':'), 427 | E('td', { 'class': 'td left' }, ('current' in smartObject.temperature) ? 428 | smartObject.temperature.current + ' °C' : null), 429 | ]), 430 | E('tr', { 'class': 'tr' }, [ 431 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Lifetime min') + ':'), 432 | E('td', { 'class': 'td left' }, ('lifetime_min' in smartObject.temperature) ? 433 | smartObject.temperature.lifetime_min + ' °C' : null), 434 | ]), 435 | E('tr', { 'class': 'tr' }, [ 436 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Lifetime max') + ':'), 437 | E('td', { 'class': 'td left' }, ('lifetime_max' in smartObject.temperature) ? 438 | smartObject.temperature.lifetime_max + ' °C' : null), 439 | ]), 440 | E('tr', { 'class': 'tr' }, [ 441 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Recommended min') + ':'), 442 | E('td', { 'class': 'td left' }, ('op_limit_min' in smartObject.temperature) ? 443 | smartObject.temperature.op_limit_min + ' °C' : null), 444 | ]), 445 | E('tr', { 'class': 'tr' }, [ 446 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Recommended max') + ':'), 447 | E('td', { 'class': 'td left' }, ('op_limit_max' in smartObject.temperature) ? 448 | smartObject.temperature.op_limit_max + ' °C' : null), 449 | ]), 450 | E('tr', { 'class': 'tr' }, [ 451 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Limit min') + ':'), 452 | E('td', { 'class': 'td left' }, ('limit_min' in smartObject.temperature) ? 453 | smartObject.temperature.limit_min + ' °C' : null), 454 | ]), 455 | E('tr', { 'class': 'tr' }, [ 456 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Limit max') + ':'), 457 | E('td', { 'class': 'td left' }, ('limit_max' in smartObject.temperature) ? 458 | smartObject.temperature.limit_max + ' °C' : null), 459 | ]), 460 | ]) 461 | ]); 462 | }, 463 | 464 | createSctTempArea(smartObject) { 465 | let device = smartObject.device.name; 466 | let deviceTime = smartObject.local_time.time_t; 467 | let intervalMin = smartObject.ata_sct_temperature_history.logging_interval_minutes; 468 | let intervalSec = intervalMin * 60; 469 | let dataSize = smartObject.ata_sct_temperature_history.size; 470 | let tempData = smartObject.ata_sct_temperature_history.table; 471 | let dataUnits = []; 472 | let tempMin = tempData.reduce( 473 | (min, current) => (current < min && current !== null) ? current : min, 474 | Infinity); 475 | let tempMax = tempData.reduce( 476 | (max, current) => (current > max && current !== null) ? current : max, 477 | -Infinity); 478 | let tempDiff = tempMax - tempMin; 479 | 480 | let i = dataSize - 1; 481 | while(i >= 0) { 482 | if(deviceTime % intervalSec == 0) { 483 | dataUnits.push([i, tempData[i], new Date(deviceTime * 1000)]); 484 | i--; 485 | }; 486 | deviceTime--; 487 | }; 488 | dataUnits.reverse(); 489 | 490 | // GRAPH 491 | 492 | let svgWidth = 900; 493 | let svgHeight = 300; 494 | let tempValueMul = (tempDiff >= 60) ? 3 : Math.round(svgHeight / (tempDiff + 20)); 495 | let tempMinimalValue = (tempMin > 10) ? tempMin - 10 : 0; 496 | let tempAxisStep = (tempDiff >= 60) ? 6 : (tempDiff >= 30) ? 4 : 2; 497 | let timeAxisStep = svgWidth / dataSize; 498 | let timeAxisInterval = Math.ceil(dataSize / 32); 499 | 500 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 501 | svg.setAttribute('width', '100%'); 502 | svg.setAttribute('height', '100%'); 503 | svg.setAttribute('version', '1.1'); 504 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 505 | 506 | // temperature line 507 | let tempLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); 508 | tempLine.setAttribute('style', 'fill:rgba(0 98 130 / 0.2); fill-opacity:1; stroke:rgba(0 98 130 / 1.0); stroke-width:1'); 509 | let tempPoints = [[0, svgHeight]]; 510 | 511 | for(let i = 0; i < dataSize; i++) { 512 | tempPoints.push([ 513 | i * timeAxisStep, 514 | (dataUnits[i][1] != null) ? 515 | (svgHeight - (dataUnits[i][1] - tempMinimalValue) * tempValueMul) : 516 | svgHeight * 2 517 | ]); 518 | }; 519 | tempPoints.push([tempPoints[tempPoints.length - 1][0], svgHeight]); 520 | tempLine.setAttribute('points', tempPoints.map(e => e.join(',')).join(' ')); 521 | svg.appendChild(tempLine); 522 | 523 | // temperature warning 524 | let lineW = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 525 | lineW.setAttribute('x1', 0); 526 | lineW.setAttribute('y1', svgHeight - (this.diskTempWarning - tempMinimalValue) * tempValueMul); 527 | lineW.setAttribute('x2', '100%'); 528 | lineW.setAttribute('y2', svgHeight - (this.diskTempWarning - tempMinimalValue) * tempValueMul); 529 | lineW.setAttribute('style', 'stroke:orange; stroke-width:0.8'); 530 | svg.appendChild(lineW); 531 | 532 | // temperature critical 533 | let lineC = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 534 | lineC.setAttribute('x1', 0); 535 | lineC.setAttribute('y1', svgHeight - (this.diskTempCritical - tempMinimalValue) * tempValueMul); 536 | lineC.setAttribute('x2', '100%'); 537 | lineC.setAttribute('y2', svgHeight - (this.diskTempCritical - tempMinimalValue) * tempValueMul); 538 | lineC.setAttribute('style', 'stroke:red; stroke-width:0.7'); 539 | svg.appendChild(lineC); 540 | 541 | // time labels 542 | let j = 0; 543 | for(let i = 0; i < svgWidth; i += timeAxisStep * timeAxisInterval) { 544 | let line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 545 | line.setAttribute('x1', i); 546 | line.setAttribute('y1', 0); 547 | line.setAttribute('x2', i); 548 | line.setAttribute('y2', '100%'); 549 | line.setAttribute('style', 'stroke:rgba(122,122,122,0.2); stroke-width:1'); 550 | svg.appendChild(line); 551 | let text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 552 | text.setAttribute('x', i + 6); 553 | text.setAttribute('y', 0); 554 | text.setAttribute('style', 'fill:rgba(122,122,122,0.5); font-family:monospace; font-size:12px; font-weight:bold; writing-mode:vertical-rl'); 555 | if(i >= 2 * timeAxisStep * timeAxisInterval) { 556 | text.appendChild(document.createTextNode('%02d.%02d %02d:%02d'.format( 557 | dataUnits[j][2].getDate(), 558 | dataUnits[j][2].getMonth() + 1, 559 | dataUnits[j][2].getHours(), 560 | dataUnits[j][2].getMinutes() 561 | ))); 562 | }; 563 | j += timeAxisInterval; 564 | svg.appendChild(text); 565 | if(j >= dataSize) { 566 | break; 567 | }; 568 | }; 569 | 570 | // temperature labels 571 | let c = 0; 572 | for(let i = svgHeight; i > 0; i -= tempValueMul * tempAxisStep) { 573 | let line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 574 | line.setAttribute('x1', 0); 575 | line.setAttribute('y1', i); 576 | line.setAttribute('x2', '100%'); 577 | line.setAttribute('y2', i); 578 | line.setAttribute('style', 'stroke:rgba(122,122,122,0.2); stroke-width:1'); 579 | svg.appendChild(line); 580 | let text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 581 | text.setAttribute('x', 0); 582 | text.setAttribute('y', i - 3); 583 | text.setAttribute('style', 'fill:#eee; font-family:monospace; font-size:14px; text-shadow:1px 1px 1px #000'); 584 | if(c % 2 == 0) { 585 | text.appendChild(document.createTextNode(((svgHeight - i) / tempValueMul) + tempMinimalValue + ' °C')); 586 | }; 587 | svg.appendChild(text); 588 | c++; 589 | }; 590 | 591 | // temperature min/max, log interval 592 | let text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 593 | text.setAttribute('x', svgWidth / 3); 594 | text.setAttribute('y', svgHeight - 10); 595 | text.setAttribute('style', 'fill:#eee; font-family:monospace; font-size:12px; text-shadow:1px 1px 1px #000'); 596 | text.appendChild(document.createTextNode(`Interval:${intervalMin}m Tmin:${tempMin}°C Tmax:${tempMax}°C`)); 597 | svg.appendChild(text); 598 | 599 | // TABLE 600 | 601 | dataUnits = dataUnits.filter((e, i, a) => { 602 | return e[1] != ((a[i - 1] !== undefined) && a[i - 1][1]); 603 | }); 604 | 605 | let sctTempTable = E('table', { 'class': 'table' }, 606 | E('tr', { 'class': 'tr table-titles' }, [ 607 | E('th', { 'class': 'th left', 'style': 'width:33%' }, _('Index')), 608 | E('th', { 'class': 'th left', 'style': 'width:33%' }, _('Estimated time')), 609 | E('th', { 'class': 'th left' }, _('Temperature') + ' °C'), 610 | ]) 611 | ); 612 | 613 | for(let [num, temp, date] of dataUnits) { 614 | if(temp === null) { 615 | continue; 616 | }; 617 | sctTempTable.append( 618 | E('tr', { 619 | 'class': (temp >= this.diskTempCritical) ? 'tr disks-info-err' : 620 | (temp >= this.diskTempWarning) ? 'tr disks-info-warn' : 'tr', 621 | }, [ 622 | E('td', { 'class': 'td left', 'data-title': _('Index') }, 623 | num), 624 | E('td', { 'class': 'td left', 'data-title': _('Estimated time') }, 625 | '%d-%02d-%02d %02d:%02d'.format( 626 | date.getFullYear(), 627 | date.getMonth() + 1, 628 | date.getDate(), 629 | date.getHours(), 630 | date.getMinutes() 631 | )), 632 | E('td', { 'class': 'td left', 'data-title': _('Temperature') + ' °C' }, 633 | temp), 634 | ]) 635 | ); 636 | }; 637 | 638 | let deviceNormalized = device.replace(/\//g, '-'); 639 | let loggingIntervalValue = E('input', { 640 | 'id' : 'logging_interval_value' + deviceNormalized, 641 | 'type' : 'text', 642 | 'class' : 'cbi-input-text', 643 | 'style' : 'width:4em !important; min-width:4em !important', 644 | 'maxlength' : 4, 645 | 'value' : 1, 646 | 'placeholder' : '1-1440', 647 | }); 648 | ui.addValidator(loggingIntervalValue, 'range(1,1440)', false); 649 | 650 | return E([ 651 | E('div', { 'class': 'cbi-value' }, [ 652 | E('h3', {}, 653 | `${_('SCT temperature history')} (${_('interval')}: ${intervalMin} ${_('min')}.):`), 654 | E('div', { 'style': 'width:100%; min-height:' + (svgHeight + 20) + 'px; overflow:auto; margin-top:0.2em' }, 655 | E('div', { 656 | 'style': 'width:' + svgWidth + 'px; height:' + svgHeight + 'px; margin:auto', 657 | }, svg) 658 | ), 659 | E('div', { 'style': 'width:100%; max-height:20em; overflow:auto; margin-top:0.2em' }, 660 | sctTempTable 661 | ), 662 | ]), 663 | 664 | E('div', { 'class': 'cbi-value' }, [ 665 | E('label', { 'class': 'cbi-value-title', 'for': 'logging_interval_value' + deviceNormalized }, 666 | _('Set logging interval') + ' (' + _('min') + ')'), 667 | E('div', { 'class': 'cbi-value-field' }, loggingIntervalValue), 668 | ]), 669 | E('div', { 'class': 'cbi-value' }, [ 670 | E('label', { 'class': 'cbi-value-title', 'for': 'logging_interval_type' + deviceNormalized }, 671 | _('Preserve across power cycles')), 672 | E('div', { 'class': 'cbi-value-field' }, 673 | E('div', { 'class': 'cbi-checkbox' }, [ 674 | E('input', { 675 | 'type': 'checkbox', 676 | 'id' : 'logging_interval_type' + deviceNormalized, 677 | }), 678 | E('label', {}) 679 | ]) 680 | ), 681 | ]), 682 | E('div', { 'class': 'cbi-value' }, [ 683 | E('label', { 'class': 'cbi-value-title', 'for': 'apply_interval_value' + deviceNormalized }, 684 | _('Write to disk device memory') 685 | ), 686 | E('div', { 'class': 'cbi-value-field' }, [ 687 | E('div', {}, E('button', { 688 | 'class': 'btn cbi-button-apply important', 689 | 'click': ui.createHandlerFn(this, this.setSctTempLogInterval, device), 690 | }, _('Apply'))), 691 | E('input', { 692 | 'id' : 'apply_interval_value' + deviceNormalized, 693 | 'type': 'hidden', 694 | }), 695 | ]), 696 | E('hr'), 697 | ]), 698 | 699 | ]); 700 | }, 701 | 702 | createDeviceStatistics(statObject) { 703 | let statsArea = E('div', { 'class': 'cbi-value' }, 704 | E('h3', {}, _('Device statistics') + ':') 705 | ); 706 | for(let page of statObject.pages) { 707 | if(!page || !Array.isArray(page.table) || page.table.length == 0) { 708 | continue; 709 | }; 710 | let pageTableTitle = E('h5', 711 | { 'style': 'width:100% !important; text-align:left !important' }, 712 | _(page.name) 713 | ); 714 | let pageTable = E('table', { 'class': 'table' }); 715 | for(let entry of page.table) { 716 | pageTable.append( 717 | E('tr', { 'class': 'tr' }, [ 718 | E('td', { 'class': 'td left', 'style': 'width:50%' }, _(entry.name) + ':'), 719 | (page.number == 7 && entry.offset == 8) ? 720 | E('td', { 721 | 'class': (entry.value >= this.ssdEnduranceWarning) ? 722 | 'td left disks-info-warn' : 'td left', 723 | }, 724 | E('div', { 725 | 'class': 'cbi-progressbar', 726 | 'title': entry.value + '%', 727 | 'data-tooltip': _('May not be supported by some devices...'), 728 | }, 729 | E('div', { 'style': 'width:' + entry.value + '%' }) 730 | ) 731 | ) 732 | : 733 | E('td', { 'class': 'td left' }, entry.value), 734 | ]) 735 | ); 736 | }; 737 | statsArea.append(pageTableTitle); 738 | statsArea.append(pageTable); 739 | }; 740 | return statsArea; 741 | }, 742 | 743 | createDeviceTable(smartObject) { 744 | return E('div', { 'class': 'cbi-value' }, [ 745 | E('h3', {}, _('Device') + ':'), 746 | E('table', { 'class': 'table' }, [ 747 | E('tr', { 'class': 'tr' }, [ 748 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Model Family') + ':'), 749 | E('td', { 'class': 'td left' }, smartObject.model_family), 750 | ]), 751 | E('tr', { 'class': 'tr' }, [ 752 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Device Model') + ':'), 753 | E('td', { 'class': 'td left' }, smartObject.model_name), 754 | ]), 755 | E('tr', { 'class': 'tr' }, [ 756 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Serial Number') + ':'), 757 | E('td', { 'class': 'td left' }, smartObject.serial_number), 758 | ]), 759 | E('tr', { 'class': 'tr' }, [ 760 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('LU WWN Device Id') + ':'), 761 | E('td', { 'class': 'td left' }, ('wwn' in smartObject) ? 762 | Object.values(smartObject.wwn).map( 763 | e => e.toString(16)).join(' ') : null), 764 | ]), 765 | E('tr', { 'class': 'tr' }, [ 766 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Firmware Version') + ':'), 767 | E('td', { 'class': 'td left' }, smartObject.firmware_version), 768 | ]), 769 | E('tr', { 'class': 'tr' }, [ 770 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('User Capacity') + ':'), 771 | E('td', { 'class': 'td left' }, ('user_capacity' in smartObject) ? 772 | `${smartObject.user_capacity.bytes} ${_('bytes')} [${(smartObject.user_capacity.bytes / 1e9).toFixed()} ${_('Gb')}] (${smartObject.user_capacity.blocks} ${_('blocks')})` 773 | : null), 774 | ]), 775 | E('tr', { 'class': 'tr' }, [ 776 | E('td', { 'class': 'td left', 'style': 'width:33%' }, `${_('Sector Size')} (${_('logical/physical')}):`), 777 | E('td', { 'class': 'td left' }, ('logical_block_size' in smartObject) ? 778 | `${smartObject.logical_block_size} ${_('bytes')} / ${smartObject.physical_block_size} ${_('bytes')}` 779 | : null), 780 | ]), 781 | E('tr', { 'class': 'tr' }, [ 782 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Rotation Rate') + ':'), 783 | E('td', { 'class': 'td left' }, (smartObject.rotation_rate == 0) ? 784 | _('Solid State Device') : smartObject.rotation_rate), 785 | ]), 786 | E('tr', { 'class': 'tr' }, [ 787 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Form Factor') + ':'), 788 | E('td', { 'class': 'td left' }, ('form_factor' in smartObject) ? 789 | smartObject.form_factor.name : null), 790 | ]), 791 | E('tr', { 'class': 'tr' }, [ 792 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Device is') + ':'), 793 | E('td', { 'class': 'td left' }, smartObject.in_smartctl_database ? 794 | _('In smartctl database [for details use: -P show]') : 795 | _('Not in smartctl database [for details use: -P showall]')), 796 | ]), 797 | E('tr', { 'class': 'tr' }, [ 798 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('ATA Version is') + ':'), 799 | E('td', { 'class': 'td left' }, ('ata_version' in smartObject) ? 800 | smartObject.ata_version.string : null), 801 | ]), 802 | E('tr', { 'class': 'tr' }, [ 803 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('SATA Version is') + ':'), 804 | E('td', { 'class': 'td left' }, ('sata_version' in smartObject) ? 805 | smartObject.sata_version.string : null), 806 | ]), 807 | E('tr', { 'class': 'tr' }, [ 808 | E('td', { 'class': 'td left', 'style': 'width:33%' }, _('Local Time is') + ':'), 809 | E('td', { 'class': 'td left' }, ('local_time' in smartObject) ? 810 | smartObject.local_time.asctime : null), 811 | ]), 812 | ]) 813 | ]); 814 | }, 815 | 816 | load() { 817 | this.restoreSettingsFromLocalStorage(); 818 | return L.resolveDefault(this.callDevices(), []); 819 | }, 820 | 821 | render(devicesData) { 822 | this.devices = devicesData.devices; 823 | 824 | let devicesNode = E('div', { 'class': 'cbi-section fade-in' }, 825 | E('div', { 'class': 'cbi-section-node' }, 826 | E('div', { 'class': 'cbi-value' }, 827 | E('em', {}, _('No devices detected')) 828 | ) 829 | ) 830 | ); 831 | 832 | if(this.devices && this.devices.length > 0) { 833 | devicesNode = E('div', { 'class': 'cbi-section fade-in' }, 834 | E('div', { 'class': 'cbi-section-node' }, 835 | E('div', { 'class': 'cbi-value' }, 836 | E('em', { 'class': 'spinning' }, _('Collecting data...')) 837 | ) 838 | ) 839 | ); 840 | 841 | Promise.all( 842 | this.devices.map(device => this.getDeviceData(device)) 843 | ).then(data => { 844 | let devicesTabs = E('div', { 'class': 'cbi-section fade-in' }, 845 | E('div', { 'class': 'cbi-section-node' }, 846 | E('div', { 'class': 'cbi-value' }, [ 847 | E('div', { 'style': 'width:100%; text-align:right !important' }, 848 | E('button', { 849 | 'class': 'btn', 850 | 'click': () => window.location.reload(), 851 | }, _('Refresh devices')) 852 | ) 853 | ]) 854 | ) 855 | ); 856 | 857 | let tabsContainer = E('div', { 'class': 'cbi-section-node cbi-section-node-tabbed' }); 858 | devicesTabs.append(tabsContainer); 859 | 860 | for(let i = 0; i < data.length; i++) { 861 | let deviceName = data[i][0]; 862 | let smart = data[i][1]; 863 | let fdisk = devicesData.fdisk[deviceName]; 864 | let df = devicesData.df[deviceName]; 865 | let deviceTab = E('div', { 866 | 'data-tab' : i, 867 | 'data-tab-title': deviceName, 868 | }); 869 | tabsContainer.append(deviceTab); 870 | 871 | let deviceNormalized = deviceName.replace(/\//g, '-'); 872 | let deviceTypeSelect = E('select', 873 | { 874 | 'id' : 'device_type' + deviceNormalized, 875 | 'change': (ev) => { 876 | this.deviceType[deviceName] = ev.target.value || this.availDeviceTypes[0].name; 877 | this.saveSettingsToLocalStorage(); 878 | window.location.reload(); 879 | }, 880 | } 881 | ); 882 | for(let i of this.availDeviceTypes) { 883 | deviceTypeSelect.append(E('option', { 'value': i.name }, i.title)); 884 | }; 885 | deviceTypeSelect.value = this.deviceType[deviceName] || this.availDeviceTypes[0].name; 886 | 887 | deviceTab.append( 888 | E('div', { 'class': 'cbi-value' }, [ 889 | E('label', { 'class': 'cbi-value-title', 'for': 'device_type' + deviceNormalized }, 890 | _('Device type')), 891 | E('div', { 'class': 'cbi-value-field' }, 892 | deviceTypeSelect 893 | ), 894 | ]), 895 | ); 896 | 897 | if(fdisk && df) { 898 | deviceTab.append(this.createDiskTable(fdisk, df)); 899 | }; 900 | 901 | if(smart) { 902 | let smartObject = smart; 903 | 904 | try { 905 | smartObject = JSON.parse(smart); 906 | } catch(err) {}; 907 | 908 | this.diskTempWarning = ( 909 | smartObject.temperature && smartObject.temperature.op_limit_max || this.diskTempWarningDefault); 910 | this.diskTempCritical = ( 911 | smartObject.temperature && smartObject.temperature.limit_max || this.diskTempCriticalDefault); 912 | 913 | if('smart_status' in smartObject && 'ata_smart_attributes' in smartObject && 914 | Array.isArray(smartObject.ata_smart_attributes.table) && 915 | smartObject.ata_smart_attributes.table.length > 0) { 916 | deviceTab.append(this.createSmartTable(smartObject)); 917 | }; 918 | if('ata_smart_error_log' in smartObject) { 919 | if(smartObject.ata_smart_error_log.summary.table) { 920 | deviceTab.append(this.createErrorLog(smartObject.ata_smart_error_log.summary.table)); 921 | }; 922 | }; 923 | if('temperature' in smartObject) { 924 | deviceTab.append(this.createTempTable(smartObject)); 925 | }; 926 | if('ata_sct_temperature_history' in smartObject && 927 | Array.isArray(smartObject.ata_sct_temperature_history.table) && 928 | smartObject.ata_sct_temperature_history.table.length > 0) { 929 | deviceTab.append(this.createSctTempArea(smartObject)); 930 | }; 931 | if('ata_device_statistics' in smartObject && 932 | Array.isArray(smartObject.ata_device_statistics.pages) && 933 | smartObject.ata_device_statistics.pages.length > 0) { 934 | deviceTab.append(this.createDeviceStatistics(smartObject.ata_device_statistics)); 935 | }; 936 | if('device' in smartObject) { 937 | deviceTab.append(this.createDeviceTable(smartObject)); 938 | }; 939 | }; 940 | }; 941 | 942 | ui.tabs.initTabGroup(tabsContainer.children); 943 | devicesNode.replaceWith(devicesTabs); 944 | }).catch(e => ui.addNotification(null, E('p', {}, e.message))); 945 | }; 946 | 947 | return E([ 948 | E('h2', { 'class': 'fade-in' }, _('Disk Devices')), 949 | E('div', { 'class': 'cbi-section-descr fade-in' }, 950 | _("Status of connected disk devices.")), 951 | devicesNode, 952 | ]); 953 | }, 954 | 955 | handleSaveApply: null, 956 | handleSave : null, 957 | handleReset : null, 958 | }); 959 | --------------------------------------------------------------------------------