status.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. <span class="ts-icon is-positive is-heading is-check-icon" style="color: var(--ts-positive-500);"></span>
  5. <span i18n>Looks Good
  6. // 看起來不錯
  7. </span>
  8. </div>
  9. <p 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.
  10. // 此狀態顯示了這個儲存節點在磁碟健康和其他系統條件方面的整體情況。 有關詳細資訊,請參閱系統分析報告。
  11. </p>
  12. <a href="#!" class="ts-button is-outlined" style="color: var(--ts-gray-50)" i18n>
  13. Analytics Report
  14. // 分析報告
  15. </a>
  16. </div>
  17. </div>
  18. <!-- Network IO -->
  19. <div class="ts-box has-top-spaced-large is-rounded is-padded ">
  20. <div class="ts-content">
  21. <div class="ts-header" i18n>Real-time Network IO
  22. // 即時網路流量
  23. </div>
  24. <div id="networkActWrapper" class="has-top-spaced-large" style="position: relative;">
  25. <canvas id="networkActivity"></canvas>
  26. </div>
  27. <div id="networkActivityPlaceHolder" class="ts-blankslate is-secondary" style="display:none;">
  28. <div class="header" i18n>Graph Render Paused
  29. // 已暫停圖表運算
  30. </div>
  31. <div class="description" i18n>Graph resumes after resizing or refocus
  32. // 當頁面調整大小或重新聚焦後,圖表將恢復運算
  33. </div>
  34. </div>
  35. </div>
  36. <div class="ts-content is-dense">
  37. <i class="ts-icon is-end-spaced is-circle-down-icon" style="color: #1890ff;"></i>
  38. <span i18n>Inbound Traffic
  39. // 進站流量
  40. </span>
  41. <i class="ts-icon is-end-spaced has-start-spaced-large is-circle-up-icon" style="color: #52c41a;"></i>
  42. <span i18n>Outbound Traffic
  43. // 出站流量
  44. </span>
  45. </div>
  46. </div>
  47. <!-- Disk SMARTs -->
  48. <div class="has-top-spaced-large is-padded">
  49. <div id="disk-smart-overview" class="ts-grid has-top-spaced-large is-relaxed is-3-columns is-stretched mobile:is-stacked">
  50. <div class="column">
  51. <div class="ts-content is-rounded is-padded">
  52. <div class="ts-header is-truncated is-large is-heavy" i18n>
  53. SMART Status
  54. // 磁碟健康狀態
  55. </div>
  56. <p>
  57. <span class="ts-icon is-spinning is-circle-notch-icon"></span>
  58. <span i18n>Loading
  59. // 載入中
  60. </span>
  61. </p>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. <script>
  67. // Function to format power-on hours into a human-readable format
  68. function formatPowerOnHours(hours) {
  69. const hoursInDay = 24;
  70. const hoursInMonth = hoursInDay * 30; // Approximate month duration
  71. const hoursInYear = hoursInDay * 365; // Approximate year duration
  72. if (hours >= hoursInYear) {
  73. return [(hours / hoursInYear).toFixed(1),`
  74. years
  75. // 年
  76. `];
  77. } else if (hours >= hoursInMonth) {
  78. return [(hours / hoursInMonth).toFixed(1), `
  79. months
  80. // 個月
  81. `];
  82. } else if (hours >= hoursInDay) {
  83. return [(hours / hoursInDay).toFixed(1), `
  84. days
  85. // 天
  86. `];
  87. } else {
  88. return [hours.toFixed(1), `
  89. hours
  90. // 個小時
  91. `];
  92. }
  93. }
  94. // Function to format power-on hours into a human-readable format
  95. function evaluateDriveHealth(info) {
  96. if (!info) return 'unknown';
  97. // Shortcut for IsHealthy flag from backend
  98. if (!info.IsHealthy) return 'not_healthy';
  99. // Thresholds based on SMART data experience
  100. const thresholds = {
  101. reallocated: 10, // more than 10 sectors or blocks is a red flag
  102. pending: 1, // any pending sectors is worrying
  103. uncorrectable: 1, // same
  104. udmaCrc: 10, // interface problems
  105. powerCycleHigh: 1000, // maybe indicates hardware or power issues
  106. wearLevel: 1000, // beyond this, flash wear is a concern
  107. };
  108. let issues = 0;
  109. if (info.ReallocatedSectors > thresholds.reallocated ||
  110. info.ReallocateNANDBlocks > thresholds.reallocated) {
  111. // Reallocated sectors or blocks
  112. if (info.ReallocatedSectors > thresholds.reallocated * 3) {
  113. return 'not_healthy';
  114. }
  115. issues++;
  116. }
  117. if (info.PendingSectors >= thresholds.pending) issues++;
  118. if (info.UncorrectableErrors >= thresholds.uncorrectable) issues++;
  119. if (info.UDMACRCErrors >= thresholds.udmaCrc) issues++;
  120. if (info.WearLevelingCount >= thresholds.wearLevel) issues++;
  121. // SSDs may silently degrade with increasing wear even if no reallocation yet
  122. if (info.IsSSD && info.WearLevelingCount > 0 && info.WearLevelingCount < 100) {
  123. return 'attention';
  124. }
  125. if (issues === 0) {
  126. return 'healthy';
  127. } else if (issues === 1) {
  128. return 'attention';
  129. } else {
  130. return 'not_healthy';
  131. }
  132. }
  133. function initDiskSmartHealthOverview(){
  134. $.get("./api/smart/health/all", function(data){
  135. $("#disk-smart-overview").html("");
  136. for (var i = 0; i < data.length; i++){
  137. let disk = data[i];
  138. let healthState = evaluateDriveHealth(disk);
  139. let iconClass = ``;
  140. let iconColor = ``;
  141. let tsBoxExtraCss = ``;
  142. if (healthState == "healthy"){
  143. iconClass = "ts-icon is-positive is-heading is-circle-check-icon";
  144. iconColor = "var(--ts-positive-500)";
  145. }else if (healthState == "attention"){
  146. iconClass = "ts-icon is-warning is-heading is-circle-exclamation-icon";
  147. iconColor = "var(--ts-warning-500)";
  148. }else if (healthState == "not_healthy"){
  149. iconClass = "ts-icon is-danger is-heading is-circle-xmark-icon";
  150. iconColor = "var(--ts-gray-300)";
  151. tsBoxExtraCss = `background-color: var(--ts-negative-400);`;
  152. }else{
  153. iconClass = "ts-icon is-heading is-circle-question-icon";
  154. iconColor = "var(--ts-gray-500)";
  155. }
  156. let poweronDuration = formatPowerOnHours(disk.PowerOnHours);
  157. $("#disk-smart-overview").append(`<div class="column">
  158. <div class="ts-box ts-content is-rounded is-padded" style="${tsBoxExtraCss}">
  159. <div class="ts-header is-truncated is-heavy">
  160. ${disk.DeviceModel}
  161. </div>
  162. <div class="ts-text has-top-spaced-small">
  163. <span class="ts-badge">/dev/${disk.DeviceName}</span>
  164. <span class="ts-badge">${disk.SerialNumber}</span>
  165. </div>
  166. <div class="ts-grid is-evenly-divided has-top-spaced-large">
  167. <div class="column">
  168. <div class="ts-text is-label" i18n>
  169. Power-on Time
  170. // 運行時間
  171. </div>
  172. <div class="ts-statistic">
  173. <div class="value">${poweronDuration[0]}</div>
  174. <div class="unit" i18n>${poweronDuration[1]}</div>
  175. </div>
  176. </div>
  177. <div class="column">
  178. <div class="ts-text is-label" i18n>Power Cycles
  179. // 開機次數
  180. </div>
  181. <div class="ts-statistic">
  182. <div class="value">${disk.PowerCycleCount}</div>
  183. <div class="unit" i18n>
  184. // 次</div>
  185. </div>
  186. </div>
  187. </div>
  188. <div class="symbol">
  189. <span style="color: ${iconColor}; opacity: 0.4; z-index: 0;" class="${iconClass}"></span>
  190. </div>
  191. </div>
  192. </div>`);
  193. }
  194. if (data.length == 0){
  195. $("#disk-smart-overview").append(`<div class="column">
  196. <div class="ts-box ts-content is-rounded is-padded">
  197. <div class="ts-text" i18n>
  198. No SMART data available
  199. // 沒有可用的磁碟健康資料
  200. </div>
  201. <div class="symbol">
  202. <span style="color: var(--ts-positive-400); opacity: 0.4; z-index: 0;" class="ts-icon is-circle-check-icon"></span>
  203. </div>
  204. </div>
  205. </div>`);
  206. }
  207. relocale();
  208. });
  209. }
  210. $(document).ready(function(){
  211. initDiskSmartHealthOverview();
  212. });
  213. </script>
  214. <!-- Network IO Chart -->
  215. <script src="./js/chart.js"></script>
  216. <script>
  217. /*
  218. Render Network Activity Graph
  219. */
  220. let rxValues = [];
  221. let txValues = [];
  222. let dataCount = 300;
  223. let timestamps = [];
  224. for(var i = 0; i < dataCount; i++){
  225. timestamps.push(new Date(Date.now() + i * 1000).toLocaleString().replace(',', ''));
  226. }
  227. function fetchData() {
  228. $.ajax({
  229. url: './api/info/netstat?array=true',
  230. success: function(data){
  231. if (rxValues.length == 0){
  232. rxValues.push(...data.Rx);
  233. }else{
  234. rxValues.push(data.Rx[dataCount-1]);
  235. rxValues.shift();
  236. }
  237. if (txValues.length == 0){
  238. txValues.push(...data.Tx);
  239. }else{
  240. txValues.push(data.Tx[dataCount-1]);
  241. txValues.shift();
  242. }
  243. timestamps.push(new Date(Date.now()).toLocaleString().replace(',', ''));
  244. timestamps.shift();
  245. updateChart();
  246. }
  247. })
  248. }
  249. function formatBandwidth(bps) {
  250. const KBPS = 1000;
  251. const MBPS = 1000 * KBPS;
  252. const GBPS = 1000 * MBPS;
  253. if (bps >= GBPS) {
  254. return (bps / GBPS).toFixed(1) + " Gbps";
  255. } else if (bps >= MBPS) {
  256. return (bps / MBPS).toFixed(1) + " Mbps";
  257. } else if (bps >= KBPS) {
  258. return (bps / KBPS).toFixed(1) + " Kbps";
  259. } else {
  260. return bps.toFixed(1) + " bps";
  261. }
  262. }
  263. function changeScaleTextColor(color){
  264. networkStatisticChart.options.scales.y.ticks.color = color;
  265. networkStatisticChart.update();
  266. }
  267. var networkStatisticChart;
  268. function initChart(){
  269. $.get("./api/info/netstat", function(data){
  270. networkStatisticChart = new Chart(
  271. document.getElementById('networkActivity'),
  272. {
  273. type: 'line',
  274. responsive: true,
  275. resizeDelay: 300,
  276. options: {
  277. animation: false,
  278. maintainAspectRatio: false,
  279. bezierCurve: true,
  280. tooltips: {enabled: false},
  281. hover: {mode: null},
  282. //stepped: 'middle',
  283. plugins: {
  284. legend: {
  285. display: false,
  286. position: "right",
  287. },
  288. title: {
  289. display: false,
  290. text: 'Network Statistic'
  291. },
  292. },
  293. scales: {
  294. x: {
  295. display: false,
  296. },
  297. y: {
  298. display: true,
  299. scaleLabel: {
  300. display: true,
  301. labelString: 'Value'
  302. },
  303. ticks: {
  304. stepSize: 10000000,
  305. callback: function(label, index, labels) {
  306. return formatBandwidth(parseInt(label));
  307. },
  308. color: $("html").hasClass("is-dark") ? "#ffffff" : "#000000",
  309. },
  310. gridLines: {
  311. display: true
  312. }
  313. }
  314. }
  315. },
  316. data: {
  317. labels: timestamps,
  318. datasets: [
  319. {
  320. label: 'In (bps)',
  321. data: rxValues,
  322. borderColor: "#1890ff",
  323. borderWidth: 1,
  324. backgroundColor: '#1890ff',
  325. fill: true,
  326. pointStyle: false,
  327. },
  328. {
  329. label: 'Out (bps)',
  330. data: txValues,
  331. borderColor: '#52c41a',
  332. borderWidth: 1,
  333. backgroundColor: '#52c41a',
  334. fill: true,
  335. pointStyle: false,
  336. }
  337. ]
  338. }
  339. }
  340. );
  341. });
  342. }
  343. function updateChart() {
  344. //Do not remove these 3 lines, it will cause memory leak
  345. if (typeof(networkStatisticChart) == "undefined"){
  346. return;
  347. }
  348. networkStatisticChart.data.datasets[0].data = rxValues;
  349. networkStatisticChart.data.datasets[1].data = txValues;
  350. networkStatisticChart.data.labels = timestamps;
  351. if (networkStatisticChart != undefined){
  352. networkStatisticChart.update();
  353. }
  354. }
  355. function updateChartSize(){
  356. let newSize = $("#networkActWrapper").width() - 300;
  357. if (window.innerWidth > 750){
  358. newSize = window.innerWidth - $(".toolbar").width() - 500;
  359. }else{
  360. newSize = $("#networkActWrapper").width() - 500;
  361. }
  362. if (networkStatisticChart != undefined){
  363. networkStatisticChart.resize(newSize, 200);
  364. }
  365. }
  366. function handleChartAccumulateResize(){
  367. $("#networkActivity").hide();
  368. $("#networkActivityPlaceHolder").show();
  369. if (chartResizeTimeout != undefined){
  370. clearTimeout(chartResizeTimeout);
  371. }
  372. chartResizeTimeout = setTimeout(function(){
  373. chartResizeTimeout = undefined;
  374. $("#networkActivityPlaceHolder").hide();
  375. $("#networkActivity").show();
  376. updateChartSize();
  377. }, 300);
  378. }
  379. var chartResizeTimeout;
  380. window.addEventListener('resize', () => {
  381. handleChartAccumulateResize();
  382. });
  383. window.addEventListener("focus", function(event){
  384. handleChartAccumulateResize();
  385. });
  386. //Initialize chart data
  387. initChart();
  388. fetchData();
  389. setInterval(fetchData, 1000);
  390. setTimeout(function(){
  391. handleChartAccumulateResize();
  392. }, 1000);
  393. </script>