status.html 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. <div class="ts-content is-rounded is-padded has-top-spaced-large" style="background: var(--ts-gray-800); color: var(--ts-gray-50)">
  2. <div style="max-width: 480px">
  3. <div class="ts-header is-huge is-heavy">
  4. <div class="sysstatus_good">
  5. <span class="ts-icon is-positive is-heading is-check-icon" style="color: var(--ts-positive-500);"></span>
  6. <span i18n>Looks Good
  7. // 看起來不錯
  8. </span>
  9. </div>
  10. <div class="sysstatus_attention" style="display:none;">
  11. <span class="ts-icon is-warning is-heading is-exclamation-icon" style="color: var(--ts-warning-600);"></span>
  12. <span i18n>Attention Required
  13. // 需要注意
  14. </span>
  15. </div>
  16. <div class="sysstatus_bad" style="display:none;">
  17. <span class="ts-icon is-negative is-heading is-xmark-icon" style="color: var(--ts-negative-500);"></span>
  18. <span i18n>Critical Error
  19. // 嚴重錯誤
  20. </span>
  21. </div>
  22. </div>
  23. <p class="sysstatus_good" i18n>This status shows you the general idea on how this storage node is doing in terms of disk health and other system conditions. See system analytic report for more details.
  24. // 此狀態顯示了這個儲存節點在磁碟健康和其他系統條件方面的整體情況。 有關詳細資訊,請參閱系統分析報告。
  25. </p>
  26. <p class="sysstatus_attention" style="display:none;" i18n>Some disks are failing soon. Check the SMART status of the disks for more details.
  27. // 某些磁碟的壽命即將結束。 請檢查磁碟的 SMART 狀態。
  28. </p>
  29. <p class="sysstatus_bad" style="display:none;" i18n>All disks are not healthy or failed. Replace the disks as soon as possible.
  30. // 所有磁碟都不健康或已損壞。 請儘快更換磁碟。
  31. </p>
  32. <a href="#!" class="ts-button is-outlined" style="color: var(--ts-gray-50)" i18n>
  33. Analytics Report
  34. // 分析報告
  35. </a>
  36. </div>
  37. </div>
  38. <!-- Disk SMARTs -->
  39. <div class="has-top-spaced-large is-padded">
  40. <div id="disk-smart-overview" class="ts-grid has-top-spaced-large is-relaxed is-3-columns is-stretched mobile:is-stacked">
  41. <div class="column">
  42. <div class="ts-content is-rounded is-padded">
  43. <div class="ts-header is-truncated is-large is-heavy" i18n>
  44. SMART Status
  45. // 磁碟健康狀態
  46. </div>
  47. <p>
  48. <span class="ts-icon is-spinning is-circle-notch-icon"></span>
  49. <span i18n>Loading
  50. // 載入中
  51. </span>
  52. </p>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- Network IO -->
  58. <div class="ts-box has-top-spaced-large is-rounded is-padded ">
  59. <div class="ts-content">
  60. <div class="ts-header" i18n>Real-time Network IO
  61. // 即時網路流量
  62. </div>
  63. <div id="networkActWrapper" class="has-top-spaced-large" style="position: relative;">
  64. <canvas id="networkActivity"></canvas>
  65. </div>
  66. <div id="networkActivityPlaceHolder" class="ts-blankslate is-secondary" style="display:none;">
  67. <div class="header" i18n>Graph Render Paused
  68. // 已暫停圖表運算
  69. </div>
  70. <div class="description" i18n>Graph resumes after resizing or refocus
  71. // 當頁面調整大小或重新聚焦後,圖表將恢復運算
  72. </div>
  73. </div>
  74. </div>
  75. <div class="ts-content is-dense">
  76. <i class="ts-icon is-end-spaced is-circle-down-icon" style="color: #1890ff;"></i>
  77. <span i18n>Inbound Traffic
  78. // 進站流量
  79. </span>
  80. <i class="ts-icon is-end-spaced has-start-spaced-large is-circle-up-icon" style="color: #52c41a;"></i>
  81. <span i18n>Outbound Traffic
  82. // 出站流量
  83. </span>
  84. </div>
  85. </div>
  86. <!-- Network Interface -->
  87. <div class="has-top-spaced-large is-padded">
  88. <div class="ts-content">
  89. <div class="ts-header is-truncated is-large is-heavy" i18n>Network Interfaces
  90. // 網路介面
  91. </div>
  92. <p i18n>List of network interfaces and their IP addresses.
  93. // 網路介面及其 IP 位址列表。
  94. </p>
  95. </div>
  96. <table class="ts-table is-striped">
  97. <thead>
  98. <tr>
  99. <th>ID</th>
  100. <th i18n>iface name
  101. // 介面名稱
  102. </th>
  103. <th i18n>IP Address
  104. // IP 位址
  105. </th>
  106. </tr>
  107. </thead>
  108. <tbody id="network-interface-list">
  109. <tr>
  110. <td colspan="3">
  111. <span class="ts-icon is-spinning is-circle-notch-icon"></span>
  112. <span i18n>Loading
  113. // 載入中
  114. </span>
  115. </td>
  116. </tr>
  117. </tbody>
  118. </table>
  119. </div>
  120. <script>
  121. /* Network Interface */
  122. function updateNetworkInterfaceTable() {
  123. $.get("./api/info/iface", function(data) {
  124. const tableBody = $("#network-interface-list");
  125. tableBody.empty(); // Clear existing rows
  126. data.forEach(iface => {
  127. const ipAddresses = iface.IPs ? iface.IPs.join("<br>") : '<td class="is-empty"></td>';
  128. const row = `
  129. <tr>
  130. <td>${iface.ID}</td>
  131. <td>${iface.Name}</td>
  132. ${iface.IPs ? "<td>" + ipAddresses + "</td>" : '<td class="is-empty"></td>'}
  133. </tr>
  134. `;
  135. tableBody.append(row);
  136. });
  137. });
  138. }
  139. // Call the function to populate the table initially
  140. updateNetworkInterfaceTable();
  141. /* SMART DISK HEALTH STATUS */
  142. // Function to format power-on hours into a human-readable format
  143. function formatPowerOnHours(hours) {
  144. const hoursInDay = 24;
  145. const hoursInMonth = hoursInDay * 30; // Approximate month duration
  146. const hoursInYear = hoursInDay * 365; // Approximate year duration
  147. if (hours >= hoursInYear) {
  148. return [(hours / hoursInYear).toFixed(1),`
  149. years
  150. // 年
  151. `];
  152. } else if (hours >= hoursInMonth) {
  153. return [(hours / hoursInMonth).toFixed(1), `
  154. months
  155. // 個月
  156. `];
  157. } else if (hours >= hoursInDay) {
  158. return [(hours / hoursInDay).toFixed(1), `
  159. days
  160. // 天
  161. `];
  162. } else {
  163. return [hours.toFixed(1), `
  164. hours
  165. // 個小時
  166. `];
  167. }
  168. }
  169. // Function to format power-on hours into a human-readable format
  170. function evaluateDriveHealth(info) {
  171. if (!info) return 'unknown';
  172. // Shortcut for IsHealthy flag from backend
  173. if (!info.IsHealthy) return 'not_healthy';
  174. // Thresholds based on SMART data experience
  175. const thresholds = {
  176. reallocated: 10, // more than 10 sectors or blocks is a red flag
  177. pending: 1, // any pending sectors is worrying
  178. uncorrectable: 1, // same
  179. udmaCrc: 10, // interface problems
  180. powerCycleHigh: 1000, // maybe indicates hardware or power issues
  181. wearLevel: 1000, // beyond this, flash wear is a concern
  182. };
  183. let issues = 0;
  184. if (info.ReallocatedSectors > thresholds.reallocated ||
  185. info.ReallocateNANDBlocks > thresholds.reallocated) {
  186. // Reallocated sectors or blocks
  187. if (info.ReallocatedSectors > thresholds.reallocated * 3) {
  188. return 'not_healthy';
  189. }
  190. issues++;
  191. }
  192. if (info.PendingSectors >= thresholds.pending) issues++;
  193. if (info.UncorrectableErrors >= thresholds.uncorrectable) issues++;
  194. if (info.UDMACRCErrors >= thresholds.udmaCrc) issues++;
  195. if (info.WearLevelingCount >= thresholds.wearLevel) issues++;
  196. // SSDs may silently degrade with increasing wear even if no reallocation yet
  197. if (info.IsSSD && info.WearLevelingCount > 0 && info.WearLevelingCount < 100) {
  198. return 'attention';
  199. }
  200. if (issues === 0) {
  201. return 'healthy';
  202. } else if (issues === 1) {
  203. return 'attention';
  204. } else {
  205. return 'not_healthy';
  206. }
  207. }
  208. function getOverallSystemHealth(){
  209. }
  210. function initDiskSmartHealthOverview(){
  211. $.get("./api/smart/health/all", function(data){
  212. $("#disk-smart-overview").html("");
  213. let good_count = 0;
  214. let attention_count = 0;
  215. let bad_count = 0;
  216. for (var i = 0; i < data.length; i++){
  217. let disk = data[i];
  218. let healthState = evaluateDriveHealth(disk);
  219. let iconClass = ``;
  220. let iconColor = ``;
  221. let tsBoxExtraCss = ``;
  222. if (healthState == "healthy"){
  223. iconClass = "ts-icon is-positive is-heading is-circle-check-icon";
  224. iconColor = "var(--ts-positive-500)";
  225. good_count++;
  226. }else if (healthState == "attention"){
  227. iconClass = "ts-icon is-warning is-heading is-circle-exclamation-icon";
  228. iconColor = "var(--ts-warning-500)";
  229. attention_count++;
  230. }else if (healthState == "not_healthy"){
  231. iconClass = "ts-icon is-danger is-heading is-circle-xmark-icon";
  232. iconColor = "var(--ts-gray-300)";
  233. tsBoxExtraCss = `background-color: var(--ts-negative-400);`;
  234. bad_count++;
  235. }else{
  236. iconClass = "ts-icon is-heading is-circle-question-icon";
  237. iconColor = "var(--ts-gray-500)";
  238. }
  239. let poweronDuration = formatPowerOnHours(disk.PowerOnHours);
  240. $("#disk-smart-overview").append(`<div class="column">
  241. <div class="ts-box ts-content is-rounded is-padded" style="${tsBoxExtraCss}">
  242. <div class="ts-header is-truncated is-heavy">
  243. ${disk.DeviceModel}
  244. </div>
  245. <div class="ts-text has-top-spaced-small">
  246. <span class="ts-badge">/dev/${disk.DeviceName}</span>
  247. <span class="ts-badge">${disk.SerialNumber}</span>
  248. </div>
  249. <div class="ts-grid is-evenly-divided has-top-spaced-large">
  250. <div class="column">
  251. <div class="ts-text is-label" i18n>
  252. Power-on Time
  253. // 運行時間
  254. </div>
  255. <div class="ts-statistic">
  256. <div class="value">${poweronDuration[0]}</div>
  257. <div class="unit" i18n>${poweronDuration[1]}</div>
  258. </div>
  259. </div>
  260. <div class="column">
  261. <div class="ts-text is-label" i18n>Power Cycles
  262. // 開機次數
  263. </div>
  264. <div class="ts-statistic">
  265. <div class="value">${disk.PowerCycleCount}</div>
  266. <div class="unit" i18n>
  267. // 次</div>
  268. </div>
  269. </div>
  270. </div>
  271. <div class="symbol">
  272. <span style="color: ${iconColor}; opacity: 0.4; z-index: 0;" class="${iconClass}"></span>
  273. </div>
  274. </div>
  275. </div>`);
  276. }
  277. if (data.length == 0){
  278. $("#disk-smart-overview").append(`<div class="column">
  279. <div class="ts-box ts-content is-rounded is-padded">
  280. <div class="ts-text" i18n>
  281. No SMART data available
  282. // 沒有可用的磁碟健康資料
  283. </div>
  284. <div class="symbol">
  285. <span style="color: var(--ts-positive-400); opacity: 0.4; z-index: 0;" class="ts-icon is-circle-check-icon"></span>
  286. </div>
  287. </div>
  288. </div>`);
  289. }
  290. // Update the overall system health status
  291. if (bad_count == data.length || bad_count + attention_count == data.length){
  292. //All disks are bad
  293. $(".sysstatus_bad").show();
  294. $(".sysstatus_attention").hide();
  295. $(".sysstatus_good").hide();
  296. }else if (bad_count > 0){
  297. $(".sysstatus_bad").hide();
  298. $(".sysstatus_attention").show();
  299. $(".sysstatus_good").hide();
  300. }else{
  301. //All or some disks are good but should not be effecting the system
  302. $(".sysstatus_bad").hide();
  303. $(".sysstatus_attention").hide();
  304. $(".sysstatus_good").show();
  305. }
  306. relocale();
  307. });
  308. }
  309. $(document).ready(function(){
  310. initDiskSmartHealthOverview();
  311. });
  312. </script>
  313. <!-- Network IO Chart -->
  314. <script src="./js/chart.js"></script>
  315. <script>
  316. /*
  317. Render Network Activity Graph
  318. */
  319. let rxValues = [];
  320. let txValues = [];
  321. let dataCount = 300;
  322. let timestamps = [];
  323. for(var i = 0; i < dataCount; i++){
  324. timestamps.push(new Date(Date.now() + i * 1000).toLocaleString().replace(',', ''));
  325. }
  326. function fetchData() {
  327. $.ajax({
  328. url: './api/info/netstat?array=true',
  329. success: function(data){
  330. if (rxValues.length == 0){
  331. rxValues.push(...data.Rx);
  332. }else{
  333. rxValues.push(data.Rx[dataCount-1]);
  334. rxValues.shift();
  335. }
  336. if (txValues.length == 0){
  337. txValues.push(...data.Tx);
  338. }else{
  339. txValues.push(data.Tx[dataCount-1]);
  340. txValues.shift();
  341. }
  342. timestamps.push(new Date(Date.now()).toLocaleString().replace(',', ''));
  343. timestamps.shift();
  344. updateChart();
  345. }
  346. })
  347. }
  348. function formatBandwidth(bps) {
  349. const KBPS = 1000;
  350. const MBPS = 1000 * KBPS;
  351. const GBPS = 1000 * MBPS;
  352. if (bps >= GBPS) {
  353. return (bps / GBPS).toFixed(1) + " Gbps";
  354. } else if (bps >= MBPS) {
  355. return (bps / MBPS).toFixed(1) + " Mbps";
  356. } else if (bps >= KBPS) {
  357. return (bps / KBPS).toFixed(1) + " Kbps";
  358. } else {
  359. return bps.toFixed(1) + " bps";
  360. }
  361. }
  362. function changeScaleTextColor(color){
  363. networkStatisticChart.options.scales.y.ticks.color = color;
  364. networkStatisticChart.update();
  365. }
  366. var networkStatisticChart;
  367. function initChart(){
  368. $.get("./api/info/netstat", function(data){
  369. networkStatisticChart = new Chart(
  370. document.getElementById('networkActivity'),
  371. {
  372. type: 'line',
  373. responsive: true,
  374. resizeDelay: 300,
  375. options: {
  376. animation: false,
  377. maintainAspectRatio: false,
  378. bezierCurve: true,
  379. tooltips: {enabled: false},
  380. hover: {mode: null},
  381. //stepped: 'middle',
  382. plugins: {
  383. legend: {
  384. display: false,
  385. position: "right",
  386. },
  387. title: {
  388. display: false,
  389. text: 'Network Statistic'
  390. },
  391. },
  392. scales: {
  393. x: {
  394. display: false,
  395. },
  396. y: {
  397. display: true,
  398. scaleLabel: {
  399. display: true,
  400. labelString: 'Value'
  401. },
  402. ticks: {
  403. stepSize: 10000000,
  404. callback: function(label, index, labels) {
  405. return formatBandwidth(parseInt(label));
  406. },
  407. color: $("html").hasClass("is-dark") ? "#ffffff" : "#000000",
  408. },
  409. gridLines: {
  410. display: true
  411. }
  412. }
  413. }
  414. },
  415. data: {
  416. labels: timestamps,
  417. datasets: [
  418. {
  419. label: 'In (bps)',
  420. data: rxValues,
  421. borderColor: "#1890ff",
  422. borderWidth: 1,
  423. backgroundColor: '#1890ff',
  424. fill: true,
  425. pointStyle: false,
  426. },
  427. {
  428. label: 'Out (bps)',
  429. data: txValues,
  430. borderColor: '#52c41a',
  431. borderWidth: 1,
  432. backgroundColor: '#52c41a',
  433. fill: true,
  434. pointStyle: false,
  435. }
  436. ]
  437. }
  438. }
  439. );
  440. });
  441. }
  442. function updateChart() {
  443. //Do not remove these 3 lines, it will cause memory leak
  444. if (typeof(networkStatisticChart) == "undefined"){
  445. return;
  446. }
  447. networkStatisticChart.data.datasets[0].data = rxValues;
  448. networkStatisticChart.data.datasets[1].data = txValues;
  449. networkStatisticChart.data.labels = timestamps;
  450. if (networkStatisticChart != undefined){
  451. networkStatisticChart.update();
  452. }
  453. }
  454. function updateChartSize(){
  455. let newSize = $("#networkActWrapper").width() - 300;
  456. if (window.innerWidth > 750){
  457. newSize = window.innerWidth - $(".toolbar").width() - 500;
  458. }else{
  459. newSize = $("#networkActWrapper").width() - 500;
  460. }
  461. if (networkStatisticChart != undefined){
  462. networkStatisticChart.resize(newSize, 200);
  463. }
  464. }
  465. function handleChartAccumulateResize(){
  466. $("#networkActivity").hide();
  467. $("#networkActivityPlaceHolder").show();
  468. if (chartResizeTimeout != undefined){
  469. clearTimeout(chartResizeTimeout);
  470. }
  471. chartResizeTimeout = setTimeout(function(){
  472. chartResizeTimeout = undefined;
  473. $("#networkActivityPlaceHolder").hide();
  474. $("#networkActivity").show();
  475. updateChartSize();
  476. }, 300);
  477. }
  478. var chartResizeTimeout;
  479. window.addEventListener('resize', () => {
  480. handleChartAccumulateResize();
  481. });
  482. window.addEventListener("focus", function(event){
  483. handleChartAccumulateResize();
  484. });
  485. //Initialize chart data
  486. initChart();
  487. fetchData();
  488. setInterval(fetchData, 1000);
  489. setTimeout(function(){
  490. handleChartAccumulateResize();
  491. }, 1000);
  492. </script>