guardia-messenger/app/(tabs)/self_healing.tsx

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 },
});