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:
1 parent
5ca0e6d83a
commit
c68aef4444
150 files changed
+1038
-690
No files matched your search
@@ -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); }
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user