status.html 21 KB

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