114 lines
6.0 KiB
TypeScript
114 lines
6.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Switch } from 'react-native';
|
||
import { ITSM_BASE } from '../../services/api';
|
||
|
||
interface OpsTask { id: string; name: string; type: string; status: string; autonomous: boolean; next_run?: string; last_result?: string }
|
||
|
||
const TASKS: OpsTask[] = [
|
||
{ id: 'T01', name: '야간 로그 정리', type: 'maintenance', status: 'scheduled', autonomous: true, next_run: '오늘 02:00' },
|
||
{ id: 'T02', name: '주간 보안 스캔', type: 'security', status: 'completed', autonomous: true, last_result: '취약점 0개 발견' },
|
||
{ id: 'T03', name: '스냅샷 백업', type: 'backup', status: 'running', autonomous: true },
|
||
{ id: 'T04', name: '용량 예측 보고서', type: 'analytics', status: 'pending', autonomous: false },
|
||
];
|
||
|
||
export default function AutonomousOpsScreen() {
|
||
const [tasks, setTasks] = useState(TASKS);
|
||
const [autonomyLevel, setAutonomyLevel] = useState(72);
|
||
const [fullAuto, setFullAuto] = useState(false);
|
||
const [opsLog, setOpsLog] = useState([
|
||
{ time: '02:14', msg: '로그 아카이브 완료 — 3.2GB 확보', type: 'success' },
|
||
{ time: '03:00', msg: 'DB 스냅샷 완료 (app-db-01)', type: 'success' },
|
||
{ time: '07:30', msg: 'CPU 이상 감지 — 자동 재시작 실행', type: 'warning' },
|
||
]);
|
||
|
||
const statusColor = (s: string) => ({ running: '#00A0C8', completed: '#44bb44', scheduled: '#ffbb00', pending: '#888', failed: '#ff4444' })[s] || '#888';
|
||
const statusIcon = (s: string) => ({ running: '⟳', completed: '✅', scheduled: '⏱', pending: '⏸', failed: '❌' })[s] || '?';
|
||
|
||
const toggleTask = async (id: string) => {
|
||
const task = tasks.find(t => t.id === id);
|
||
if (!task) return;
|
||
try {
|
||
await fetch(`${ITSM_BASE}/api/ops-automation/tasks/${id}/toggle`, { method: 'POST' });
|
||
setTasks(prev => prev.map(t => t.id === id ? { ...t, autonomous: !t.autonomous } : t));
|
||
} catch {
|
||
setTasks(prev => prev.map(t => t.id === id ? { ...t, autonomous: !t.autonomous } : t));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<ScrollView style={s.container}>
|
||
<Text style={s.title}>자율 운영</Text>
|
||
<Text style={s.sub}>GUARDiA가 인프라를 자율적으로 운영합니다</Text>
|
||
|
||
<View style={s.autonomyCard}>
|
||
<Text style={s.autonomyLabel}>자립도</Text>
|
||
<Text style={s.autonomyVal}>{autonomyLevel}%</Text>
|
||
<View style={s.barBg}>
|
||
<View style={[s.barFill, { width: `${autonomyLevel}%` }]} />
|
||
</View>
|
||
<Text style={s.autonomyDesc}>목표: 85% (2027 Q1)</Text>
|
||
</View>
|
||
|
||
<View style={s.card}>
|
||
<View style={s.row}>
|
||
<Text style={s.label}>완전 자율 운영 모드</Text>
|
||
<Switch value={fullAuto} onValueChange={setFullAuto} trackColor={{ true: '#44bb44', false: '#333' }} />
|
||
</View>
|
||
{fullAuto && <Text style={s.warningText}>⚠️ 모든 운영 작업이 승인 없이 자동 실행됩니다</Text>}
|
||
</View>
|
||
|
||
<Text style={s.sectionTitle}>자율 운영 작업</Text>
|
||
{tasks.map(task => (
|
||
<View key={task.id} style={s.taskCard}>
|
||
<View style={s.taskHeader}>
|
||
<Text style={[s.statusIcon, { color: statusColor(task.status) }]}>{statusIcon(task.status)}</Text>
|
||
<Text style={s.taskName}>{task.name}</Text>
|
||
<Switch value={task.autonomous} onValueChange={() => toggleTask(task.id)} trackColor={{ true: '#00A0C8', false: '#333' }} />
|
||
</View>
|
||
<View style={s.taskMeta}>
|
||
<View style={s.typeBadge}><Text style={s.typeText}>{task.type}</Text></View>
|
||
{task.next_run && <Text style={s.metaText}>다음 실행: {task.next_run}</Text>}
|
||
{task.last_result && <Text style={s.metaText}>{task.last_result}</Text>}
|
||
</View>
|
||
</View>
|
||
))}
|
||
|
||
<Text style={s.sectionTitle}>오늘 자율 운영 로그</Text>
|
||
{opsLog.map((log, i) => (
|
||
<View key={i} style={s.logRow}>
|
||
<Text style={s.logTime}>{log.time}</Text>
|
||
<Text style={[s.logMsg, { color: log.type === 'success' ? '#44bb44' : log.type === 'warning' ? '#ffbb00' : '#ff4444' }]}>{log.msg}</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 },
|
||
autonomyCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
|
||
autonomyLabel: { color: '#888', fontSize: 12, marginBottom: 4 },
|
||
autonomyVal: { color: '#00A0C8', fontSize: 40, fontWeight: '700', marginBottom: 8 },
|
||
barBg: { height: 10, backgroundColor: '#333', borderRadius: 5, marginBottom: 8 },
|
||
barFill: { height: 10, backgroundColor: '#00A0C8', borderRadius: 5 },
|
||
autonomyDesc: { color: '#888', fontSize: 12 },
|
||
card: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 16, borderWidth: 1, borderColor: '#333' },
|
||
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||
label: { color: '#fff', fontSize: 15 },
|
||
warningText: { color: '#ffbb00', fontSize: 12, marginTop: 8 },
|
||
sectionTitle: { color: '#fff', fontSize: 16, fontWeight: '700', marginBottom: 10 },
|
||
taskCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
|
||
taskHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 },
|
||
statusIcon: { fontSize: 18, marginRight: 10 },
|
||
taskName: { color: '#fff', fontWeight: '600', flex: 1 },
|
||
taskMeta: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||
typeBadge: { backgroundColor: '#003366', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6 },
|
||
typeText: { color: '#00A0C8', fontSize: 11 },
|
||
metaText: { color: '#888', fontSize: 12 },
|
||
logRow: { flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#1A1F2E' },
|
||
logTime: { color: '#555', fontSize: 12, marginRight: 12, minWidth: 40 },
|
||
logMsg: { flex: 1, fontSize: 13 },
|
||
});
|