feat: Agent采集uptime, 后端持久化kernel/uptime/diskIO, 前端磁盘BPS+IOPS展示

- Agent: system.go 添加 readUptime(), SystemStat 携带 Uptime
- Backend: Server 模型新增 Kernel 字段, ReceiveReport 持久化 kernel+uptime
- Backend: /agent/latest 返回 kernel/uptime/port_info/disk_io_info/disk_total
- Frontend: 磁盘监控改为 BPS+IOPS (4张卡片), 系统信息修复内核/运行时长/监听端口/磁盘容量
- 图表单位从 MB/s 改为 KB/s, 添加 formatBPS 工具函数
This commit is contained in:
attacker committed 2026-06-19 01:10:00 +00:00
1 parent 31c2e338a3
commit 5949ec5d0c
48 files changed
+2182 -956

No files matched your search

@@ -5,12 +5,15 @@ import {
DialogDescription,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import { Maximize2, Minimize2, X } from 'lucide-react';
import { Maximize2, Minimize2, X, Key, Shield } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useUserStore } from '@/store';
import { credentialApi, serverCredentialApi } from '@/services/api';
import { toast } from 'sonner';
interface TerminalDialogProps {
open: boolean;
@@ -18,6 +21,8 @@ interface TerminalDialogProps {
serverId: number | null;
serverName: string;
serverIp: string;
/** 服务器是否有可用的认证凭据(密码或SSH密钥) */
hasAuth?: boolean;
}
/**
@@ -30,6 +35,7 @@ export function TerminalDialog({
serverId,
serverName,
serverIp,
hasAuth = true,
}: TerminalDialogProps) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
@@ -38,9 +44,44 @@ export function TerminalDialog({
const [connected, setConnected] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
// 连接终端
// 凭据选择状态(hasAuth=false 时显示) // Logan.Li
const [credentials, setCredentials] = useState<any[]>([]);
const [selectedCredId, setSelectedCredId] = useState<string>('');
const [assigning, setAssigning] = useState(false);
const [authReady, setAuthReady] = useState(hasAuth); // 认证就绪标志
// 加载凭据列表
useEffect(() => {
if (!open || !serverId) return;
if (!open || hasAuth) return;
credentialApi.list().then((res: any) => setCredentials(res?.list || [])).catch(() => {});
}, [open, hasAuth]);
// 关联凭据
const handleAssignCredential = async () => {
if (!selectedCredId || !serverId) return;
try {
setAssigning(true);
await serverCredentialApi.assign(serverId, Number(selectedCredId));
toast.success('凭据已关联');
setAuthReady(true);
} catch (e: any) {
toast.error(e?.message || '关联失败');
} finally {
setAssigning(false);
}
};
// 重置状态
useEffect(() => {
if (!open) {
setAuthReady(hasAuth);
setSelectedCredId('');
}
}, [open, hasAuth]);
// 连接终端(authReady=true 时才执行)
useEffect(() => {
if (!open || !serverId || !authReady) return;
// 创建终端
const term = new XTerm({
@@ -115,7 +156,7 @@ export function TerminalDialog({
wsRef.current = null;
setConnected(false);
};
}, [open, serverId]);
}, [open, serverId, authReady]);
// 用 ResizeObserver 监听容器实际尺寸变化,自动 fit 终端
// 这比 window resize 或 setTimeout 更可靠:
@@ -189,12 +230,47 @@ export function TerminalDialog({
</button>
</div>
</div>
{/* 凭据选择器(无认证凭据时显示) */}
{!authReady && (
<div className="p-6 flex flex-col items-center justify-center gap-4" style={{ height: '480px' }}>
<Shield className="h-12 w-12 text-amber-400" />
<div className="text-center">
<p className="text-[var(--dg-text-primary)] font-medium mb-1">服务器未配置认证凭据</p>
<p className="text-sm text-[var(--dg-text-muted)]">请选择一个凭据用于 SSH 连接</p>
</div>
{credentials.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">暂无可用凭据,请先去「凭证配置」添加</p>
) : (
<select
value={selectedCredId}
onChange={(e) => setSelectedCredId(e.target.value)}
className="w-64 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
>
<option value="">-- 选择凭据 --</option>
{credentials.map((c: any) => (
<option key={c.id} value={String(c.id)}>
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
</option>
))}
</select>
)}
<Button
onClick={handleAssignCredential}
disabled={!selectedCredId || assigning}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{assigning ? '关联中...' : '确认连接'}
</Button>
</div>
)}
{/* 终端容器 */}
<div
ref={containerRef}
className="bg-[#0a0e14]"
style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}
/>
{authReady && (
<div
ref={containerRef}
className="bg-[#0a0e14]"
style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}
/>
)}
</DialogContent>
</Dialog>
);