4c1dc4082e
Build and Push Images / build-and-push (push) Has been cancelled
이전 세션들에서 작업된 아래 범위를 모두 포함: Fleet 서브시스템 (src/fleet/) - fleetDeviceService / fleetCommandService / fleetDeploymentService / fleetReleaseService - fleetMetricsService, fleetScriptService, fleetEdgeConfigService - Edge 디바이스 관리, 커맨드 발행, 배포/릴리스, 스크립트 동기화 Collector 확장 - centralMqttForwarder / centralForwarderConfigService - equipmentStateService, pythonHookRunner, scriptCache - Modbus/OPC-UA/S7/XGT 프로토콜 클라이언트 - targetDbIntrospection (저장 DB 조회) Routes / API - automationDashboardRoutes, centralForwarderRoutes, equipmentStateRoutes DB - importEdgeConfig (Python cached config → Pipeline DB) - seedDataSources (external_db_connections 초기 시드) 엣지 배포 리소스 - docker/edge/Dockerfile.backend.prod, Dockerfile.frontend.prod - docker/edge/docker-compose.edge.yml 프론트엔드 - admin/automaticMng (centralForwarder, dashboard, equipmentState) - admin/fleet (commands, devices, deployments, releases, scripts, alerts) - admin/pipeline-device 개선 (저장 DB 드롭다운, 태그 매핑 등) - ExternalDbConnectionModal, ScriptsManagerDialog 등 신규 컴포넌트 - lib/api: automationDashboard, centralForwarder, equipmentState, fleet docs/ - EDGE_SERVER_STRUCTURE, FLEET_COMPLETE, FLEET_EDGE_INTEGRATION, FLEET_HOOK_INTEGRATION Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
159 lines
6.3 KiB
TypeScript
159 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
import { fleetApi, FleetAlert } from "@/lib/api/fleet";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { RefreshCw, AlertTriangle, CheckCircle2, Circle, Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
const SEVERITY_COLORS: Record<string, string> = {
|
|
info: "bg-blue-500/10 text-blue-600 border-blue-500/20",
|
|
warning: "bg-amber-500/10 text-amber-600 border-amber-500/20",
|
|
critical: "bg-red-500/10 text-red-600 border-red-500/20",
|
|
};
|
|
|
|
export default function FleetAlertsPage() {
|
|
const [alerts, setAlerts] = useState<FleetAlert[]>([]);
|
|
const [rules, setRules] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [statusFilter, setStatusFilter] = useState<"open" | "acknowledged" | "resolved">("open");
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [a, r] = await Promise.all([
|
|
fleetApi.getAlerts(statusFilter),
|
|
fleetApi.getAlertRules(),
|
|
]);
|
|
setAlerts(a.data || []);
|
|
setRules(r.data || []);
|
|
} catch { toast.error("알림 조회 실패"); }
|
|
setLoading(false);
|
|
}, [statusFilter]);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
const t = setInterval(load, 30000);
|
|
return () => clearInterval(t);
|
|
}, [load]);
|
|
|
|
const ackAlert = async (id: number) => {
|
|
try { await fleetApi.ackAlert(id); toast.success("확인 처리"); load(); }
|
|
catch { toast.error("실패"); }
|
|
};
|
|
|
|
const resolveAlert = async (id: number) => {
|
|
try { await fleetApi.resolveAlert(id); toast.success("해결 처리"); load(); }
|
|
catch { toast.error("실패"); }
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full flex-col bg-background">
|
|
<div className="shrink-0 p-6 pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-bold">Fleet 알림</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
임계값 기반 자동 알림 (CPU/메모리/디스크/오프라인)
|
|
</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={load} disabled={loading} className="gap-1">
|
|
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
|
새로고침
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 필터 */}
|
|
<div className="mt-4 flex items-center gap-2">
|
|
<Select value={statusFilter} onValueChange={(v: any) => setStatusFilter(v)}>
|
|
<SelectTrigger className="w-40"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="open">🔴 미처리</SelectItem>
|
|
<SelectItem value="acknowledged">🟡 확인됨</SelectItem>
|
|
<SelectItem value="resolved">✅ 해결됨</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<div className="text-xs text-muted-foreground">총 {alerts.length}건</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto px-6 pb-6">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="h-6 w-6 animate-spin" />
|
|
</div>
|
|
) : alerts.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground rounded-xl border border-dashed">
|
|
<CheckCircle2 className="h-10 w-10 mb-3 text-green-500" />
|
|
<p className="text-sm">해당 상태의 알림이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{alerts.map((a) => (
|
|
<div
|
|
key={a.id}
|
|
className={`rounded-lg border p-4 ${SEVERITY_COLORS[a.severity] || ""}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-3 flex-1">
|
|
<AlertTriangle className="h-5 w-5 mt-0.5 shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<p className="text-sm font-semibold">{a.title}</p>
|
|
<Badge variant="outline" className="text-[10px]">
|
|
{a.severity.toUpperCase()}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">{a.message}</p>
|
|
<div className="flex items-center gap-3 mt-2 text-[10px] text-muted-foreground">
|
|
<span>📱 {a.device_id}</span>
|
|
<span>📏 {a.metric} = {a.value} (임계 {a.threshold})</span>
|
|
<span>🕒 {new Date(a.created_at).toLocaleString("ko-KR")}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{a.status === "open" && (
|
|
<div className="flex gap-1">
|
|
<Button size="sm" variant="outline" onClick={() => ackAlert(a.id)}>
|
|
확인
|
|
</Button>
|
|
<Button size="sm" onClick={() => resolveAlert(a.id)}>
|
|
해결
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{a.status === "acknowledged" && (
|
|
<Button size="sm" onClick={() => resolveAlert(a.id)}>
|
|
해결
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 규칙 */}
|
|
<div className="mt-8">
|
|
<h2 className="text-sm font-bold mb-3">알림 규칙 ({rules.length}개)</h2>
|
|
<div className="space-y-1">
|
|
{rules.map((r) => (
|
|
<div key={r.id} className="flex items-center justify-between rounded-md border p-3 text-xs">
|
|
<div>
|
|
<span className="font-medium">{r.rule_name}</span>
|
|
<span className="text-muted-foreground ml-2">
|
|
{r.metric} {r.operator} {r.threshold}
|
|
</span>
|
|
</div>
|
|
<Badge variant="outline">{r.severity}</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|