108 lines
5.9 KiB
TypeScript
108 lines
5.9 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Switch, ActivityIndicator } from 'react-native';
|
|
import { ITSM_BASE } from '../../services/api';
|
|
|
|
interface HealingEvent { id: string; trigger: string; action: string; target: string; status: string; duration_ms: number; ts: string }
|
|
|
|
const MOCK_EVENTS: HealingEvent[] = [
|
|
{ id: 'HE-001', trigger: 'CPU > 90%', action: 'nginx 재시작', target: 'app-01', status: 'success', duration_ms: 1240, ts: new Date(Date.now() - 300000).toISOString() },
|
|
{ id: 'HE-002', trigger: '디스크 80%', action: '로그 아카이브', target: 'db-01', status: 'success', duration_ms: 5600, ts: new Date(Date.now() - 900000).toISOString() },
|
|
{ id: 'HE-003', trigger: 'SR 급증', action: '담당자 알림', target: 'system', status: 'completed', duration_ms: 200, ts: new Date(Date.now() - 1800000).toISOString() },
|
|
];
|
|
|
|
export default function SelfHealingScreen() {
|
|
const [autoHeal, setAutoHeal] = useState(true);
|
|
const [requireApproval, setRequireApproval] = useState(false);
|
|
const [events, setEvents] = useState<HealingEvent[]>(MOCK_EVENTS);
|
|
const [stats, setStats] = useState({ healed: 14, prevented: 6, success_rate: 93.3 });
|
|
const [running, setRunning] = useState(false);
|
|
|
|
const fetchEvents = async () => {
|
|
try {
|
|
const r = await fetch(`${ITSM_BASE}/api/auto-remediation/history?limit=20`);
|
|
if (r.ok) { const d = await r.json(); if (d.items?.length) setEvents(d.items); }
|
|
} catch {}
|
|
};
|
|
|
|
useEffect(() => { fetchEvents(); }, []);
|
|
|
|
const triggerManual = async () => {
|
|
setRunning(true);
|
|
try {
|
|
const r = await fetch(`${ITSM_BASE}/api/auto-remediation/trigger`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode: 'manual', scope: 'all' }),
|
|
});
|
|
if (r.ok) { await fetchEvents(); }
|
|
} catch {}
|
|
setRunning(false);
|
|
};
|
|
|
|
const statusColor = (s: string) => ({ success: '#44bb44', completed: '#44bb44', failed: '#ff4444', running: '#00A0C8', pending: '#ffbb00' })[s] || '#888';
|
|
|
|
return (
|
|
<ScrollView style={s.container}>
|
|
<Text style={s.title}>자가 치유 (Self-Healing)</Text>
|
|
<Text style={s.sub}>GUARDiA가 스스로 장애를 감지하고 복구합니다</Text>
|
|
|
|
<View style={s.statsRow}>
|
|
<View style={s.statBox}><Text style={s.statVal}>{stats.healed}</Text><Text style={s.statLbl}>자동 복구</Text></View>
|
|
<View style={s.statBox}><Text style={s.statVal}>{stats.prevented}</Text><Text style={s.statLbl}>사전 예방</Text></View>
|
|
<View style={s.statBox}><Text style={[s.statVal, { color: '#44bb44' }]}>{stats.success_rate}%</Text><Text style={s.statLbl}>성공률</Text></View>
|
|
</View>
|
|
|
|
<View style={s.card}>
|
|
<View style={s.row}>
|
|
<Text style={s.label}>자동 치유 활성화</Text>
|
|
<Switch value={autoHeal} onValueChange={setAutoHeal} trackColor={{ true: '#44bb44', false: '#333' }} />
|
|
</View>
|
|
<View style={s.row}>
|
|
<Text style={s.label}>조치 전 승인 필요</Text>
|
|
<Switch value={requireApproval} onValueChange={setRequireApproval} trackColor={{ true: '#ffbb00', false: '#333' }} />
|
|
</View>
|
|
<TouchableOpacity style={s.manualBtn} onPress={triggerManual} disabled={running}>
|
|
{running ? <ActivityIndicator color="#fff" /> : <Text style={s.manualBtnText}>⚡ 수동 진단 실행</Text>}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<Text style={s.sectionTitle}>자가 치유 이력</Text>
|
|
{events.map(ev => (
|
|
<View key={ev.id} style={s.eventCard}>
|
|
<View style={s.eventHeader}>
|
|
<View style={[s.statusDot, { backgroundColor: statusColor(ev.status) }]} />
|
|
<Text style={[s.statusText, { color: statusColor(ev.status) }]}>{ev.status}</Text>
|
|
<Text style={s.durationText}>{ev.duration_ms}ms</Text>
|
|
</View>
|
|
<Text style={s.trigger}>트리거: {ev.trigger}</Text>
|
|
<Text style={s.action}>조치: {ev.action} → {ev.target}</Text>
|
|
<Text style={s.ts}>{new Date(ev.ts).toLocaleString()}</Text>
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#0A0E1A', padding: 16 },
|
|
title: { color: '#fff', fontSize: 20, fontWeight: '700', marginBottom: 4 },
|
|
sub: { color: '#888', fontSize: 13, marginBottom: 16 },
|
|
statsRow: { flexDirection: 'row', justifyContent: 'space-around', backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
|
|
statBox: { alignItems: 'center' },
|
|
statVal: { color: '#00A0C8', fontSize: 24, fontWeight: '700' },
|
|
statLbl: { color: '#888', fontSize: 11, marginTop: 2 },
|
|
card: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
|
|
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#222' },
|
|
label: { color: '#fff', fontSize: 15 },
|
|
manualBtn: { backgroundColor: '#003366', padding: 12, borderRadius: 10, alignItems: 'center', marginTop: 12, borderWidth: 1, borderColor: '#00A0C8' },
|
|
manualBtnText: { color: '#fff', fontWeight: '700' },
|
|
sectionTitle: { color: '#fff', fontSize: 16, fontWeight: '700', marginBottom: 12 },
|
|
eventCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
|
|
eventHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 },
|
|
statusDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 },
|
|
statusText: { fontWeight: '700', fontSize: 12, flex: 1 },
|
|
durationText: { color: '#888', fontSize: 11 },
|
|
trigger: { color: '#aaa', fontSize: 13, marginBottom: 4 },
|
|
action: { color: '#fff', fontSize: 13, fontWeight: '600', marginBottom: 4 },
|
|
ts: { color: '#555', fontSize: 11 },
|
|
});
|