// Note: This script expects csrfToken to be available globally // It should be set in the HTML template before this script loads // ========== 전역 변수 ========== let CONFIG = { grace_minutes: 60, recency_hours: 24, poll_interval_ms: 10000 }; let monitoringOn = false; let pollTimer = null; let lastRenderHash = ""; // ========== Elements ========== const $ = id => document.getElementById(id); const $body = $('jobs-body'); const $last = $('last-updated'); const $loading = $('loading-indicator'); const $btn = $('btn-refresh'); const $auto = $('autoRefreshSwitch'); const $ipInput = $('ipInput'); const $ipCount = $('ip-count'); const $btnLoad = $('btn-load-file'); const $btnApply = $('btn-apply'); const $monSw = $('monitorSwitch'); const $statusDot = $('status-dot'); const $statusText = $('status-text'); const $showCompleted = $('showCompletedSwitch'); const $pollInterval = $('poll-interval'); // Stats const $statTotal = $('stat-total'); const $statRunning = $('stat-running'); const $statCompleted = $('stat-completed'); const $statError = $('stat-error'); // ========== LocalStorage Keys ========== const LS_IPS = 'idrac_job_ips'; const LS_MON = 'idrac_monitor_on'; const LS_AUTO = 'idrac_monitor_auto'; const LS_SHOW_COMPLETED = 'idrac_show_completed'; // ========== 유틸리티 ========== function parseIps(text) { if (!text) return []; const raw = text.replace(/[,;]+/g, '\n'); const out = [], seen = new Set(); raw.split('\n').forEach(line => { const parts = line.trim().split(/\s+/); parts.forEach(p => { if (!p || p.startsWith('#')) return; if (!seen.has(p)) { seen.add(p); out.push(p); } }); }); return out; } function getIpsFromUI() { return parseIps($ipInput.value); } function setIpsToUI(ips) { $ipInput.value = (ips || []).join('\n'); updateIpCount(); } function updateIpCount() { const ips = getIpsFromUI(); $ipCount.textContent = ips.length; } function saveIps() { localStorage.setItem(LS_IPS, JSON.stringify(getIpsFromUI())); updateIpCount(); } function loadIps() { try { const v = JSON.parse(localStorage.getItem(LS_IPS) || "[]"); return Array.isArray(v) ? v : []; } catch { return []; } } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); } function progressBar(pc) { const n = parseInt(String(pc ?? "").toString().replace('%', '').trim(), 10); if (isNaN(n)) return `${escapeHtml(pc ?? "")}`; let bgClass = 'bg-info'; if (n === 100) bgClass = 'bg-success'; else if (n < 30) bgClass = 'bg-warning'; return `
${n}%`; } function badgeStatus(status, pc, recently = false) { const raw = String(status || ""); const s = raw.toLowerCase(); let cls = "bg-secondary"; let icon = "info-circle"; if (recently) { cls = "bg-success"; icon = "check-circle"; } else if (s.includes("completed")) { cls = "bg-success"; icon = "check-circle"; } else if (s.includes("running") || s.includes("progress")) { cls = "bg-info"; icon = "arrow-repeat"; } else if (s.includes("scheduled") || s.includes("pending")) { cls = "bg-warning text-dark"; icon = "clock"; } else if (s.includes("failed") || s.includes("error")) { cls = "bg-danger"; icon = "x-circle"; } const pct = parseInt(String(pc ?? "").toString().replace('%', '').trim(), 10); const pctText = isNaN(pct) ? "" : ` (${pct}%)`; const text = recently ? `${raw || "Completed"} (최근${pctText})` : `${raw || "-"}${pctText}`; return ` ${escapeHtml(text)} `; } function updateStats(items) { let total = 0, running = 0, completed = 0, error = 0; items.forEach(it => { if (!it.ok) { error++; return; } if (it.jobs && it.jobs.length) { total++; it.jobs.forEach(j => { const s = (j.Status || "").toLowerCase(); if (s.includes("running") || s.includes("progress") || s.includes("starting")) { running++; } else if (s.includes("completed") || s.includes("success")) { completed++; } else if (s.includes("failed") || s.includes("error")) { error++; } }); } }); $statTotal.textContent = items.length; $statRunning.textContent = running; $statCompleted.textContent = completed; $statError.textContent = error; } // ========== 렌더링 ========== function renderTable(items) { if (!items) return; const hash = JSON.stringify(items); if (hash === lastRenderHash) return; lastRenderHash = hash; if (!items.length) { $body.innerHTML = ` 현재 모니터링 중인 Job이 없습니다. `; updateStats([]); return; } const rows = []; for (const it of items) { if (!it.ok) { rows.push(` ${escapeHtml(it.ip)} 오류: ${escapeHtml(it.error || "Unknown")} `); continue; } if (!it.jobs || !it.jobs.length) continue; for (const j of it.jobs) { const recent = !!j.RecentlyCompleted; const timeText = j.CompletedAt ? `완료: ${escapeHtml(j.CompletedAt.split('T')[1]?.split('.')[0] || j.CompletedAt)}` : escapeHtml(j.LastUpdateTime || ""); rows.push(` ${escapeHtml(it.ip)} ${escapeHtml(j.JID || "")} ${escapeHtml(j.Name || "")} ${badgeStatus(j.Status || "", j.PercentComplete || "", recent)} ${progressBar(j.PercentComplete || "0")} ${escapeHtml(j.Message || "")} ${timeText} `); } } $body.innerHTML = rows.length ? rows.join("") : ` 현재 진행 중인 Job이 없습니다. ✅ `; updateStats(items); } // ========== 서버 요청 ========== async function fetchJobs(auto = false) { if (!monitoringOn) { $body.innerHTML = ` 모니터링이 꺼져 있습니다. 상단 스위치를 켜면 조회가 시작됩니다. `; $last.textContent = ""; updateStats([]); return; } const ips = getIpsFromUI(); if (!ips.length) { $body.innerHTML = ` IP 목록이 비어 있습니다. `; $last.textContent = ""; updateStats([]); return; } try { $loading.classList.remove('d-none'); $last.textContent = "조회 중… " + new Date().toLocaleTimeString(); const res = await fetch("/jobs/scan", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": csrfToken }, body: JSON.stringify({ ips, method: "redfish", // Redfish 사용 recency_hours: CONFIG.recency_hours, grace_minutes: CONFIG.grace_minutes, include_tracked_done: $showCompleted.checked }) }); const data = await res.json(); if (!data.ok) throw new Error(data.error || "Scan failed"); renderTable(data.items); $last.textContent = "업데이트: " + new Date().toLocaleString(); } catch (e) { $body.innerHTML = ` 로드 실패: ${escapeHtml(e.message)}
`; $last.textContent = "에러: " + new Date().toLocaleString(); console.error(e); } finally { $loading.classList.add('d-none'); } } // ========== 모니터링 제어 ========== function startAuto() { stopAuto(); pollTimer = setInterval(() => fetchJobs(true), CONFIG.poll_interval_ms); } function stopAuto() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } function updateMonitorUI() { $btn.disabled = !monitoringOn; $auto.disabled = !monitoringOn; if (monitoringOn) { $statusDot.classList.add('active'); $statusText.textContent = '모니터링 중'; } else { $statusDot.classList.remove('active'); $statusText.textContent = '모니터링 꺼짐'; } } async function setMonitoring(on) { monitoringOn = !!on; localStorage.setItem(LS_MON, monitoringOn ? "1" : "0"); updateMonitorUI(); if (!monitoringOn) { stopAuto(); $last.textContent = ""; $body.innerHTML = ` 모니터링이 꺼져 있습니다. `; lastRenderHash = ""; updateStats([]); return; } await fetchJobs(false); if ($auto.checked) startAuto(); } // ========== 초기화 ========== document.addEventListener('DOMContentLoaded', async () => { // 설정 로드 try { const res = await fetch('/jobs/config'); const data = await res.json(); if (data.ok) { CONFIG = data.config; $pollInterval.textContent = Math.round(CONFIG.poll_interval_ms / 1000); } } catch (e) { console.error("Failed to load config:", e); } // IP 복원 const savedIps = loadIps(); if (savedIps.length) { setIpsToUI(savedIps); } else { try { const res = await fetch('/jobs/iplist', { headers: { 'X-CSRFToken': csrfToken } }); const data = await res.json(); if (data.ok && data.ips) { setIpsToUI(data.ips); saveIps(); } } catch (e) { console.error(e); } } // 설정 복원 const savedMon = localStorage.getItem(LS_MON); const savedAuto = localStorage.getItem(LS_AUTO); const savedShowCompleted = localStorage.getItem(LS_SHOW_COMPLETED); $monSw.checked = savedMon === "1"; $auto.checked = savedAuto === "1"; $showCompleted.checked = savedShowCompleted !== "0"; // 이벤트 $ipInput.addEventListener('input', updateIpCount); $btn.addEventListener('click', () => { if (monitoringOn) fetchJobs(false); }); $auto.addEventListener('change', e => { localStorage.setItem(LS_AUTO, e.target.checked ? "1" : "0"); if (!monitoringOn) return; if (e.target.checked) startAuto(); else stopAuto(); }); $monSw.addEventListener('click', e => setMonitoring(e.target.checked)); $showCompleted.addEventListener('change', e => { localStorage.setItem(LS_SHOW_COMPLETED, e.target.checked ? "1" : "0"); if (monitoringOn) fetchJobs(false); }); $btnLoad.addEventListener('click', async () => { try { const res = await fetch('/jobs/iplist', { headers: { 'X-CSRFToken': csrfToken } }); const data = await res.json(); if (data.ok) { setIpsToUI(data.ips || []); saveIps(); if (monitoringOn) await fetchJobs(false); } } catch (e) { alert('IP 목록을 불러오는 중 오류가 발생했습니다: ' + e.message); } }); $btnApply.addEventListener('click', () => { saveIps(); if (monitoringOn) fetchJobs(false); }); // 초기 상태 updateMonitorUI(); updateIpCount(); if ($monSw.checked) { setMonitoring(true); } });