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: [] };