refactor(frontend): api/components/pages 目录架构统一对齐菜单域 + 日志中心修复

1. 目录架构统一(API = components = 菜单域,一域一目录 + barrel)
   - log/ 提为独立顶层域(日志中心在菜单中与监控中心平级,不再塞在 monitor/)
   - task/ 拆分原 automation.ts → cron-job / script / remote-exec
   - aiops/ 合并 ai.ts + agentCli.ts → assistant + agent-cli
   - agent/ approval/ audit/ 提升为域目录
   - auth 拆分:authApi(认证自服务) + system/user.ts(userApi+ldapApi 管理端)
   - components/automation → components/task,菜单"自动化运维"→"作业任务"
   - pages/ 按域分子目录(approval/audit/monitor),删孤儿 AuditPage.tsx
   - 所有 API 符号名不变,组件经 @/api 聚合引用,运行时行为零变化

2. Loki 查询无返回修复:aiops 容器 DNS 解析外网域名失败
   (宿主机 systemd-resolved 转发链路异常),docker-compose 显式配 dns 上游

3. ES 查询框布局:工具栏单行挤 7 控件(ES 多 150px 索引框),拆两行——
   查询输入独占一行(+查询按钮),次级控件换行 flex-wrap

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
attackerandClaude committed 2026-06-28 06:06:48 +00:00
1 parent da01cbb119
commit c0ee8e4ddd
74 files changed
+229 -216

No files matched your search

@@ -0,0 +1,247 @@
// 终端审计 — 在线会话子面板 // Logan.Li
import { useState, useEffect } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Search, RefreshCw, XOctagon, Eye, Activity, Timer, TimerOff } from 'lucide-react';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { LiveMonitorDialog } from '@/components/system/LiveMonitorDialog';
import { useTerminalSessions, useTerminalSessionMutations } from '@/hooks/api';
import type { TerminalSession } from './types';
import { formatDuration, getStatusBadge } from './helpers';
const REFRESH_SEC = 10;
interface ActiveSessionsPanelProps {
userToggledOff: boolean;
onUserToggledOff: () => void;
}
function useElapsedTimer(startTime: string | undefined): number {
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
if (!startTime) return;
const start = new Date(startTime).getTime();
const tick = () => setElapsed(Math.floor((Date.now() - start) / 1000));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [startTime]);
return elapsed;
}
function ActiveSessionRow({
session,
onKill,
onMonitor,
}: {
session: TerminalSession;
onKill: (id: number, username: string) => void;
onMonitor: (id: number) => void;
}) {
const elapsed = useElapsedTimer(session.start_time);
return (
<TableRow className="border-slate-800 hover:bg-slate-800/50">
<TableCell>
<span className="text-sm text-slate-400">{session.id}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-200">{session.username || '--'}</span>
</TableCell>
<TableCell>
<div className="flex flex-col">
<span className="text-sm text-slate-200">{session.server_name || '--'}</span>
<span className="text-xs text-slate-500">{session.server_ip}</span>
</div>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">{session.server_port}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
{session.start_time ? new Date(session.start_time).toLocaleString() : '--'}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-emerald-400 font-mono">{formatDuration(elapsed)}</span>
</TableCell>
<TableCell>{getStatusBadge(session.status)}</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-400 hover:bg-slate-800"
onClick={() => onKill(session.id, session.username)}
title="阻断会话"
>
<XOctagon className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-blue-400 hover:bg-slate-800"
onClick={() => onMonitor(session.id)}
title="实时查看"
>
<Eye className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
);
}
export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: ActiveSessionsPanelProps) {
const confirm = useConfirm();
const { killMutation } = useTerminalSessionMutations();
const { data: sessions = [], isLoading, refetch } = useTerminalSessions({
page: 1,
page_size: 100,
status: 'active',
});
const [keyword, setKeyword] = useState('');
const [autoRefresh, setAutoRefresh] = useState(() => !userToggledOff);
const [monitorOpen, setMonitorOpen] = useState(false);
const [monitorSessionId, setMonitorSessionId] = useState<number | null>(null);
// 用户未手动关闭时,回到在线会话自动开启刷新
useEffect(() => {
if (!userToggledOff) setAutoRefresh(true);
}, [userToggledOff]);
useEffect(() => {
if (!autoRefresh) return;
const id = setInterval(() => refetch(), REFRESH_SEC * 1000);
return () => clearInterval(id);
}, [autoRefresh, refetch]);
const filtered = sessions.filter((s) => {
const kw = keyword.toLowerCase();
return (
!kw ||
(s.username || '').toLowerCase().includes(kw) ||
(s.server_name || '').toLowerCase().includes(kw) ||
(s.server_ip || '').toLowerCase().includes(kw)
);
});
const handleKill = async (id: number, username: string) => {
const ok = await confirm({
title: '阻断会话',
description: `确认强制断开用户「${username}」的当前会话吗?`,
confirmText: '阻断',
variant: 'destructive',
});
if (!ok) return;
try {
await killMutation.mutateAsync(id);
} catch {
/* noop */
}
};
const openMonitor = (sessionId: number) => {
setMonitorSessionId(sessionId);
setMonitorOpen(true);
};
return (
<>
<div className="flex items-center gap-3 flex-wrap">
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="搜索用户/服务器..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
</div>
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
onClick={() => refetch()}
>
<RefreshCw className="h-4 w-4 mr-2" />
刷新
</Button>
<Button
variant={autoRefresh ? 'default' : 'outline'}
className={
autoRefresh
? 'bg-emerald-600 hover:bg-emerald-700 text-white'
: 'border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800'
}
onClick={() => {
if (autoRefresh) onUserToggledOff();
setAutoRefresh(!autoRefresh);
}}
title={autoRefresh ? `每 ${REFRESH_SEC}s 自动刷新 · 点击关闭` : '点击开启自动刷新'}
>
{autoRefresh ? (
<>
<Timer className="h-4 w-4 mr-1.5 animate-pulse" />
{REFRESH_SEC}s
</>
) : (
<>
<TimerOff className="h-4 w-4 mr-2" />
自动刷新
</>
)}
</Button>
</div>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-0">
<div className="border-t border-slate-800">
{isLoading ? (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
<Activity className="h-8 w-8 mb-3 opacity-40" />
<p className="text-sm">暂无在线会话</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">ID</TableHead>
<TableHead className="text-slate-400">用户</TableHead>
<TableHead className="text-slate-400">目标服务器</TableHead>
<TableHead className="text-slate-400">端口</TableHead>
<TableHead className="text-slate-400">开始时间</TableHead>
<TableHead className="text-slate-400">时长</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400 text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((s) => (
<ActiveSessionRow key={s.id} session={s} onKill={handleKill} onMonitor={openMonitor} />
))}
</TableBody>
</Table>
)}
</div>
</CardContent>
</Card>
<LiveMonitorDialog open={monitorOpen} onOpenChange={setMonitorOpen} sessionId={monitorSessionId} />
</>
);
}