diff --git a/public/css/style.css b/public/css/style.css index a449fc4..1c07166 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -663,6 +663,45 @@ input:checked+.slider:before { background: rgba(99, 102, 241, 0.05); } +.search-box { + position: relative; + display: flex; + align-items: center; + max-width: 240px; +} + +#serverSearchInput { + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 6px 12px 6px 34px; + color: var(--text-primary); + font-size: 0.85rem; + width: 100%; + outline: none; + transition: all 0.3s ease; +} + +#serverSearchInput:focus { + border-color: var(--accent-indigo); + background: rgba(99, 102, 241, 0.05); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.15); +} + +.search-box .search-icon { + position: absolute; + left: 10px; + width: 16px; + height: 16px; + color: var(--text-muted); + pointer-events: none; + transition: color 0.3s ease; +} + +#serverSearchInput:focus + .search-icon { + color: var(--accent-indigo); +} + .btn-icon-sm { background: var(--bg-input); border: 1px solid var(--border-color); diff --git a/public/index.html b/public/index.html index 1fec209..7650e51 100644 --- a/public/index.html +++ b/public/index.html @@ -283,6 +283,14 @@ 服务器详情
+
+
+ 硬盘总量统计 + 0 GB +
diff --git a/public/js/app.js b/public/js/app.js index 048c445..2990574 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -22,6 +22,7 @@ totalBandwidthTx: document.getElementById('totalBandwidthTx'), totalBandwidthRx: document.getElementById('totalBandwidthRx'), traffic24hRx: document.getElementById('traffic24hRx'), + serverSearchInput: document.getElementById('serverSearchInput'), traffic24hTx: document.getElementById('traffic24hTx'), traffic24hTotal: document.getElementById('traffic24hTotal'), trafficP95: document.getElementById('trafficP95'), @@ -65,6 +66,7 @@ legendTx: document.getElementById('legendTx'), p95LabelText: document.getElementById('p95LabelText'), p95TypeSelect: document.getElementById('p95TypeSelect'), + detailDiskTotal: document.getElementById('detailDiskTotal'), // Server Details Modal serverDetailModal: document.getElementById('serverDetailModal'), serverDetailClose: document.getElementById('serverDetailClose'), @@ -268,6 +270,14 @@ dom.btnResetSort.addEventListener('click', resetSort); } + // Server list search + if (dom.serverSearchInput) { + dom.serverSearchInput.addEventListener('input', () => { + currentPage = 1; // Reset page on search + renderFilteredServers(); + }); + } + // Check auth status checkAuthStatus(); @@ -624,6 +634,15 @@ filtered = allServersData.filter(s => s.source === currentSourceFilter); } + // Apply search filter + const searchTerm = (dom.serverSearchInput?.value || '').toLowerCase().trim(); + if (searchTerm) { + filtered = filtered.filter(s => + (s.job || '').toLowerCase().includes(searchTerm) || + (s.instance || '').toLowerCase().includes(searchTerm) + ); + } + // Sort servers: online first, then by currentSort filtered.sort((a, b) => { // Primary sort: Always put online servers first unless sorting by 'up' explicitly @@ -889,11 +908,17 @@ const mins = Math.floor((uptimeSec % 3600) / 60); dom.detailUptime.textContent = `${days}天 ${hours}小时 ${mins}分`; + // Disk Total + const totalDiskSize = (data.partitions || []).reduce((sum, p) => sum + (p.size || 0), 0); + if (dom.detailDiskTotal) { + dom.detailDiskTotal.textContent = formatBytes(totalDiskSize); + } + // Define metrics to show const cpuValueHtml = `
${formatPercent(data.cpuBusy)} - (IO Wait: ${data.cpuIowait.toFixed(1)}%) + (IO Wait: ${data.cpuIowait.toFixed(1)}%, Busy Others: ${data.cpuOther.toFixed(1)}%)
`; // Define metrics to show @@ -904,6 +929,7 @@ { key: 'rootFsUsedPct', label: '根分区使用率 (/)', value: formatPercent(data.rootFsUsedPct) }, { key: 'netRx', label: '网络接收速率 (RX)', value: formatBandwidth(data.netRx) }, { key: 'netTx', label: '网络发送速率 (TX)', value: formatBandwidth(data.netTx) }, + { key: 'networkTrend', label: '网络流量趋势 (24h)', value: '查看实时趋势线' }, { key: 'sockstatTcp', label: 'TCP 链接数 (Sockstat)', value: data.sockstatTcp.toFixed(0) }, { key: 'sockstatTcpMem', label: 'TCP 内存占用', value: formatBytes(data.sockstatTcpMem) } ]; @@ -1015,7 +1041,12 @@ if (metricKey.startsWith('net')) unit = 'B/s'; if (metricKey === 'sockstatTcpMem') unit = 'B'; - chart = new MetricChart(canvas, unit); + if (metricKey === 'networkTrend') { + chart = new AreaChart(canvas); + chart.padding = { top: 15, right: 15, bottom: 35, left: 65 }; + } else { + chart = new MetricChart(canvas, unit); + } currentServerDetail.charts[metricKey] = chart; } diff --git a/server/prometheus-service.js b/server/prometheus-service.js index 124ad9c..969ea24 100644 --- a/server/prometheus-service.js +++ b/server/prometheus-service.js @@ -639,11 +639,28 @@ async function getServerHistory(baseUrl, instance, job, metric, range = '1h', st sockstatTcpMem: `node_sockstat_TCP_mem{instance="${node}",job="${job}"} * 4096` }; + const rangeObj = parseRange(range, start, end); + + if (metric === 'networkTrend') { + const txExpr = metricMap.netTx; + const rxExpr = metricMap.netRx; + const [txResult, rxResult] = await Promise.all([ + queryRange(url, txExpr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step), + queryRange(url, rxExpr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step) + ]); + + if (txResult.length === 0 && rxResult.length === 0) return { timestamps: [], rx: [], tx: [] }; + + const timestamps = (txResult.length > 0 ? txResult[0] : rxResult[0]).values.map(v => v[0] * 1000); + const tx = txResult.length > 0 ? txResult[0].values.map(v => parseFloat(v[1])) : new Array(timestamps.length).fill(0); + const rx = rxResult.length > 0 ? rxResult[0].values.map(v => parseFloat(v[1])) : new Array(timestamps.length).fill(0); + + return { timestamps, tx, rx }; + } + const expr = metricMap[metric]; if (!expr) throw new Error('Invalid metric for history'); - const rangeObj = parseRange(range, start, end); - try { const result = await queryRange(url, expr, rangeObj.queryStart, rangeObj.queryEnd, rangeObj.step); if (!result || result.length === 0) return { timestamps: [], values: [] };