zioinfo-mail/itsm/static/offline.html
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
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>
2026-05-29 18:18:52 +09:00

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">&#128268;</div>
<h1>네트워크 연결 없음</h1>
<p>
GUARDiA ITSM 서버에 연결할 수 없습니다.<br>
네트워크 상태를 확인하고 다시 시도해 주세요.
</p>
<div class="actions">
<button class="btn btn-primary" onclick="retryConnection()">
&#8635; 다시 시도
</button>
<button class="btn btn-outline" onclick="goBack()">
&#8592; 뒤로 가기
</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>