feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)

- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

+7 -6
View File
@@ -15,6 +15,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Plus, Search, RefreshCw, Clock, CheckCircle, XCircle, FileText, Loader2, Eye, Check, XIcon, User, ChevronRight, Play } from 'lucide-react';
import { approvalApi } from '@/api';
import { toast } from 'sonner';
import type { JsonData } from '@/types/common';
export interface FormField {
key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea';
@@ -50,7 +51,7 @@ function parseFormData(data: string): Record<string, string> {
try { return JSON.parse(data) || {}; } catch { return {}; }
}
function getApplicant(p: any): string {
function getApplicant(p: JsonData): string {
const u = p?.applicant_user;
return u ? (u.real_name || u.username || `#${p.applicant}`) : (p?.applicant ? `#${p.applicant}` : '--');
}
@@ -80,10 +81,10 @@ export function ApprovalCategoryPage({ category, title, description, fields, tem
const fetchProcesses = useCallback(async () => {
setLoading(true);
try {
const params: any = { page_size: '100' };
const params: JsonData = { page_size: '100' };
if (category) params.category = category;
if (statusFilter) params.status = statusFilter;
const resp = (await approvalApi.getProcesses(params)) as any;
const resp = (await approvalApi.getProcesses(params)) as JsonData;
setProcesses(resp?.list ?? resp?.data?.list ?? []);
} catch { setProcesses([]); }
finally { setLoading(false); }
@@ -91,7 +92,7 @@ export function ApprovalCategoryPage({ category, title, description, fields, tem
const fetchTemplateId = useCallback(async () => {
try {
const resp = (await approvalApi.getTemplates({ type: templateCode })) as any;
const resp = (await approvalApi.getTemplates({ type: templateCode })) as JsonData;
const list = resp?.list ?? resp?.data?.list ?? [];
if (list.length > 0) setTemplateId(list[0].id);
} catch {}
@@ -141,9 +142,9 @@ export function ApprovalCategoryPage({ category, title, description, fields, tem
const openDetail = async (p: ProcessItem) => {
// Load full detail including steps
try {
const resp: any = await approvalApi.getProcesses({ page_size: 200 } as any);
const resp: JsonData = await approvalApi.getProcesses({ page_size: 200 } as JsonData);
const list = resp?.list ?? resp?.data?.list ?? [];
const full = list.find((x: any) => x.id === p.id);
const full = list.find((x: JsonData) => x.id === p.id);
setDetailItem(full || p);
} catch { setDetailItem(p); }
};
+7 -6
View File
@@ -25,6 +25,7 @@ import { DualLineChart } from './host-monitor/DualLineChart';
import { HealthProbeSection } from './host-monitor/HealthProbeSection';
import { PortPolicyDialog } from './host-monitor/PortPolicyDialog';
import { SystemInfoSection } from './host-monitor/SystemInfoSection';
import type { JsonData } from '@/types/common';
export function HostMonitor() {
const qc = useQueryClient();
@@ -83,10 +84,10 @@ export function HostMonitor() {
const metrics = useMemo(() => buildMetricsFromHistory(history, timeConfig), [history, timeConfig]);
const netInfo = parseJSON(latest?.net_info);
const portInfo: Record<string, any>[] = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []);
const portInfo: JsonData = Array.isArray(latest?.port_info) ? latest.port_info : (Array.isArray(parseJSON(latest?.port_info)) ? parseJSON(latest?.port_info) : []);
const diskIOInfo = parseJSON(latest?.disk_io_info);
const swapInfo = parseJSON(latest?.swap_info);
const processTop: Record<string, any>[] = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) : [];
const processTop: JsonData = Array.isArray(parseJSON(latest?.process_top_info)) ? parseJSON(latest?.process_top_info) : [];
const fdInfo = parseJSON(latest?.fd_info);
const csInfo = parseJSON(latest?.cs_info);
const heartbeatInfo = parseJSON(latest?.heartbeat_info);
@@ -108,7 +109,7 @@ export function HostMonitor() {
<SelectValue placeholder="选择主机..." />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
{hosts.map((h: any) => (
{hosts.map((h: JsonData) => (
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
))}
</SelectContent>
@@ -177,7 +178,7 @@ export function HostMonitor() {
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
<MetricChartCard title="Inode 使用率(%)" value={(() => {
const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : [];
const root = dArr.find((d: any) => d.mount === '/') || dArr[0];
const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0];
return root?.inode_usage_pct != null ? `${root.inode_usage_pct.toFixed(1)}%` : '--';
})()} icon={<HardDrive className="h-4 w-4 text-purple-400" />}
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} />
@@ -254,7 +255,7 @@ export function HostMonitor() {
</tr>
</thead>
<tbody>
{portInfo.map((p: any, i: number) => (
{portInfo.map((p: JsonData, i: number) => (
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-slate-800/30 transition-colors">
<td className="py-1.5 font-mono text-[var(--dg-text-primary)]">{p.port}</td>
<td className="py-1.5">
@@ -301,7 +302,7 @@ export function HostMonitor() {
</tr>
</thead>
<tbody>
{processTop.map((p: any, i: number) => {
{processTop.map((p: JsonData, i: number) => {
const cmd = p.cmdline || p.name || '?';
const cpup = p.cpu_pct != null ? Number(p.cpu_pct) : null;
const memp = p.mem_pct != null ? Number(p.mem_pct) : null;
+4 -3
View File
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/table';
import { Search, RefreshCw, LogIn, Monitor, Globe } from 'lucide-react';
import { loginLogApi } from '@/api';
import type { JsonData } from '@/types/common';
interface LoginLog {
id: number;
@@ -30,7 +31,7 @@ interface LoginLog {
created_at: string;
}
function normalizeLoginLog(raw: any): LoginLog {
function normalizeLoginLog(raw: JsonData): LoginLog {
return {
id: raw.id ?? 0,
username: raw.username ?? '',
@@ -56,9 +57,9 @@ export function LoginLogsTab() {
const fetchLogs = useCallback(async () => {
setLoading(true);
try {
const params: any = { page: 1, page_size: 100 };
const params: JsonData = { page: 1, page_size: 100 };
if (statusFilter) params.status = statusFilter === 'fail' ? 'failed' : statusFilter;
const resp = (await loginLogApi.list(params)) as any;
const resp = (await loginLogApi.list(params)) as JsonData;
const list = resp?.list ?? [];
setLogs(list.map(normalizeLoginLog));
} catch {
+4 -3
View File
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Activity, Search, Filter, BarChart3, Cpu, HardDrive, Network, Database, Plus } from 'lucide-react';
import { metricsApi } from '@/api';
import type { JsonData } from '@/types/common';
interface _MetricName {
name: string;
@@ -51,7 +52,7 @@ export function MetricsExplorer() {
// Load targets
const loadTargets = useCallback(async () => {
try {
const res: Record<string, any> = await metricsApi.getTargets({ page_size: 200 });
const res: JsonData = await metricsApi.getTargets({ page_size: 200 });
setTargets(res?.list || []);
} catch { /* empty */ }
finally { setLoading(false); }
@@ -60,7 +61,7 @@ export function MetricsExplorer() {
// Load metric names
const loadMetricNames = useCallback(async () => {
try {
const names: any = await metricsApi.getNames();
const names: string[] = await metricsApi.getNames();
setMetricNames(names || []);
} catch { /* empty */ }
}, []);
@@ -79,7 +80,7 @@ export function MetricsExplorer() {
Promise.all(
selectedMetrics.map(mn =>
metricsApi.query({ target_id: selectedTarget, metric_name: mn, from: String(from), to: String(to), limit: 200 })
.then(( data: any) => (Array.isArray(data) ? data : []))
.then((data: JsonData) => (Array.isArray(data) ? data : []))
.catch(() => [])
)
).then(results => {
+2 -1
View File
@@ -4,6 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Database, Server, Activity, Loader2, Circle } from 'lucide-react';
import { metricsApi } from '@/api';
import type { JsonData } from '@/types/common';
interface TargetInfo {
id: number;
@@ -71,7 +72,7 @@ export function MiddlewareMonitor() {
const data = await metricsApi.getTargets({ type: 'middleware', page_size: 200 }) as TargetsResponse;
setTargets(data?.list || []);
} catch (err) {
setError((err as Record<string, any>).message || '加载失败');
setError((err as JsonData).message || '加载失败');
setTargets([]);
} finally {
setLoading(false);
+5 -4
View File
@@ -45,6 +45,7 @@ import {
import { toast } from 'sonner';
import { siteApi } from '@/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { JsonData } from '@/types/common';
// 拨测协议类型(与后端 model 对齐:http, https, tcp, dns, ping)
const probeTypes = [
@@ -161,7 +162,7 @@ export function SiteMonitor() {
setRefreshing(true);
try {
const data = await siteApi.getTasks();
setTasks((data as Record<string, any>)?.list || []);
setTasks((data as JsonData)?.list || []);
} catch {
// 后端不可用时保留空状态
} finally {
@@ -206,7 +207,7 @@ export function SiteMonitor() {
loadTasks();
loadStats();
} catch (e) {
toast.error((e as Record<string, any>).message || '创建失败');
toast.error((e as JsonData).message || '创建失败');
} finally {
setSaving(false);
}
@@ -225,7 +226,7 @@ export function SiteMonitor() {
loadTasks();
loadStats();
} catch (e) {
toast.error((e as Record<string, any>).message || '删除失败');
toast.error((e as JsonData).message || '删除失败');
}
};
@@ -236,7 +237,7 @@ export function SiteMonitor() {
loadTasks();
loadStats();
} catch (e) {
toast.error((e as Record<string, any>).message || '操作失败');
toast.error((e as JsonData).message || '操作失败');
}
};
@@ -3,12 +3,13 @@ import { Activity, HardDrive } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { TabsContent } from '@/components/ui/tabs';
import { parseJSON } from './utils';
import type { JsonData } from '@/types/common';
interface SystemInfoSectionProps {
selectedHost: string;
serverInfo: any;
latest: any;
heartbeatInfo: any;
serverInfo?: JsonData;
latest?: JsonData;
heartbeatInfo?: JsonData;
portInfoCount: number;
}
@@ -67,7 +68,7 @@ export function SystemInfoSection({
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-2 flex items-center gap-2"><HardDrive className="h-4 w-4" />磁盘分区</h3>
<div className="space-y-1.5">
{parts.map((p: any, i: number) => (
{parts.map((p: JsonData, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs">
<span className="text-[var(--dg-text-muted)] w-24 font-mono truncate">{p.mount || p.mountpoint || p.device || `disk${i}`}</span>
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
+5 -5
View File
@@ -1,5 +1,6 @@
// 主机监控工具函数 // Logan.Li
import type { BucketPoint } from './types';
import type { JsonData } from '@/types/common';
export function bucketWithStats(raw: number[], timestamps: number[], targetBuckets: number): BucketPoint[] {
if (!raw || raw.length === 0) return [];
@@ -26,7 +27,7 @@ export function bucketWithStats(raw: number[], timestamps: number[], targetBucke
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function parseJSON(raw: unknown): any {
export function parseJSON(raw: unknown): JsonData {
if (!raw) return {};
if (typeof raw === 'object') return raw;
try { return JSON.parse(raw as string); } catch { return {}; }
@@ -41,7 +42,7 @@ export function formatBPS(bps: number): string {
interface TimeConfig { buckets: number; }
export function buildMetricsFromHistory(history: any[], timeConfig: TimeConfig) {
export function buildMetricsFromHistory(history: JsonData[], timeConfig: TimeConfig) {
const rawCpu: number[] = []; const rawMem: number[] = []; const rawLoad1: number[] = [];
const rawDisk: number[] = []; const rawTcp: number[] = []; const rawTw: number[] = []; const rawCw: number[] = [];
const rawRx: number[] = []; const rawTx: number[] = [];
@@ -65,7 +66,7 @@ export function buildMetricsFromHistory(history: any[], timeConfig: TimeConfig)
const diskInfoArr = parseJSON(r.disk_info);
if (Array.isArray(diskInfoArr) && diskInfoArr.length > 0) {
const rootDisk = diskInfoArr.find((d: any) => d.mount === '/') || diskInfoArr[0];
const rootDisk = diskInfoArr.find((d: JsonData) => d.mount === '/') || diskInfoArr[0];
rawInode.push(rootDisk?.inode_usage_pct || 0);
} else {
rawInode.push(0);
@@ -117,8 +118,7 @@ export function buildMetricsFromHistory(history: any[], timeConfig: TimeConfig)
};
}
export function getCurrentVal(
latest: any, netInfo: any, diskIOInfo: any, swapInfo: any, fdInfo: any,
export function getCurrentVal(latest: JsonData, netInfo: JsonData, diskIOInfo: JsonData, swapInfo: JsonData, fdInfo: JsonData,
key: string, fallback = '--',
): string {
if (!latest) return fallback;