G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
208 lines
5.0 KiB
HTML
208 lines
5.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#1a1d2e">
|
|
<title>오프라인 — GUARDiA ITSM</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #1a1d2e;
|
|
--card: #242740;
|
|
--border: #3a3d5c;
|
|
--primary: #4f8ef7;
|
|
--text: #e2e8f0;
|
|
--muted: #94a3b8;
|
|
--warn: #f59e0b;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 1rem;
|
|
padding: 2.5rem 2rem;
|
|
max-width: 420px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.icon {
|
|
font-size: 3.5rem;
|
|
line-height: 1;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.4rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.75rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
p {
|
|
color: var(--muted);
|
|
font-size: 0.95rem;
|
|
line-height: 1.6;
|
|
margin-bottom: 1.75rem;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 0.5rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border: none;
|
|
transition: opacity 0.15s;
|
|
text-decoration: none;
|
|
}
|
|
.btn:hover { opacity: 0.85; }
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: #fff;
|
|
}
|
|
.btn-outline {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-top: 1.75rem;
|
|
padding-top: 1.25rem;
|
|
border-top: 1px solid var(--border);
|
|
font-size: 0.85rem;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--warn);
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
#retry-msg {
|
|
font-size: 0.82rem;
|
|
color: var(--muted);
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
@media (min-width: 480px) {
|
|
.actions { flex-direction: row; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<div class="icon">🔌</div>
|
|
<h1>네트워크 연결 없음</h1>
|
|
<p>
|
|
GUARDiA ITSM 서버에 연결할 수 없습니다.<br>
|
|
네트워크 상태를 확인하고 다시 시도해 주세요.
|
|
</p>
|
|
|
|
<div class="actions">
|
|
<button class="btn btn-primary" onclick="retryConnection()">
|
|
↻ 다시 시도
|
|
</button>
|
|
<button class="btn btn-outline" onclick="goBack()">
|
|
← 뒤로 가기
|
|
</button>
|
|
</div>
|
|
|
|
<div id="retry-msg"></div>
|
|
|
|
<div class="status-bar">
|
|
<div class="dot" id="status-dot"></div>
|
|
<span id="status-text">오프라인</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 온라인 상태 변화 감지
|
|
function updateOnlineStatus() {
|
|
const dot = document.getElementById('status-dot');
|
|
const text = document.getElementById('status-text');
|
|
if (navigator.onLine) {
|
|
dot.style.background = '#22c55e';
|
|
dot.style.animation = 'none';
|
|
text.textContent = '연결 복구됨 — 페이지를 새로고침합니다...';
|
|
setTimeout(() => location.reload(), 1200);
|
|
} else {
|
|
dot.style.background = 'var(--warn)';
|
|
dot.style.animation = 'pulse 1.5s infinite';
|
|
text.textContent = '오프라인';
|
|
}
|
|
}
|
|
|
|
window.addEventListener('online', updateOnlineStatus);
|
|
window.addEventListener('offline', updateOnlineStatus);
|
|
updateOnlineStatus();
|
|
|
|
function retryConnection() {
|
|
const msg = document.getElementById('retry-msg');
|
|
msg.textContent = '연결 시도 중...';
|
|
fetch('/api/metrics/health', { cache: 'no-store' })
|
|
.then(r => {
|
|
if (r.ok) {
|
|
msg.textContent = '연결 성공! 이동 중...';
|
|
setTimeout(() => location.href = '/', 800);
|
|
} else {
|
|
msg.textContent = '서버 응답 오류. 잠시 후 다시 시도해 주세요.';
|
|
}
|
|
})
|
|
.catch(() => {
|
|
msg.textContent = '연결 실패. 네트워크를 확인해 주세요.';
|
|
});
|
|
}
|
|
|
|
function goBack() {
|
|
if (history.length > 1) history.back();
|
|
else location.href = '/';
|
|
}
|
|
|
|
// 자동 재시도 (30초마다)
|
|
let retryTimer = setInterval(() => {
|
|
if (navigator.onLine) {
|
|
clearInterval(retryTimer);
|
|
retryConnection();
|
|
}
|
|
}, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|