update
This commit is contained in:
@@ -58,6 +58,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
refreshServers();
|
||||
loadGroups();
|
||||
setupSocketIO();
|
||||
|
||||
// 초기 탭 설정
|
||||
showTab('servers');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
@@ -299,27 +302,27 @@ function closeUploadModal() {
|
||||
async function startMultiUpload() {
|
||||
const fileInput = document.getElementById('firmware-file');
|
||||
const serverIds = getSelectedServerIds();
|
||||
|
||||
|
||||
if (!fileInput.files[0]) {
|
||||
showMessage('파일을 선택하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileInput.files[0]);
|
||||
formData.append('server_ids', serverIds.join(','));
|
||||
|
||||
|
||||
try {
|
||||
closeUploadModal();
|
||||
showUploadProgress(serverIds);
|
||||
|
||||
|
||||
const response = await fetchWithCSRF('/idrac/api/upload-multi', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
} else {
|
||||
@@ -335,9 +338,9 @@ async function startMultiUpload() {
|
||||
function showUploadProgress(serverIds) {
|
||||
const section = document.getElementById('upload-progress-section');
|
||||
const list = document.getElementById('upload-progress-list');
|
||||
|
||||
|
||||
section.style.display = 'block';
|
||||
|
||||
|
||||
// 각 서버별 진행 바 생성
|
||||
list.innerHTML = serverIds.map(id => {
|
||||
const server = servers.find(s => s.id === id);
|
||||
@@ -354,7 +357,7 @@ function showUploadProgress(serverIds) {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
|
||||
// 요약 초기화
|
||||
document.getElementById('progress-summary').innerHTML = `
|
||||
<div class="summary-stats">
|
||||
@@ -375,13 +378,13 @@ function hideUploadProgress() {
|
||||
|
||||
function setupSocketIO() {
|
||||
// 업로드 진행 상황
|
||||
socket.on('upload_progress', function(data) {
|
||||
socket.on('upload_progress', function (data) {
|
||||
console.log('Upload progress:', data);
|
||||
|
||||
|
||||
const statusEl = document.getElementById(`status-${data.server_id}`);
|
||||
const barEl = document.getElementById(`bar-${data.server_id}`);
|
||||
const messageEl = document.getElementById(`message-${data.server_id}`);
|
||||
|
||||
|
||||
if (statusEl) statusEl.textContent = data.message;
|
||||
if (barEl) {
|
||||
barEl.style.width = data.progress + '%';
|
||||
@@ -389,18 +392,18 @@ function setupSocketIO() {
|
||||
}
|
||||
if (messageEl) messageEl.textContent = data.job_id ? `Job ID: ${data.job_id}` : '';
|
||||
});
|
||||
|
||||
|
||||
// 업로드 완료
|
||||
socket.on('upload_complete', function(data) {
|
||||
socket.on('upload_complete', function (data) {
|
||||
console.log('Upload complete:', data);
|
||||
|
||||
|
||||
const summary = data.summary;
|
||||
document.getElementById('completed-count').textContent = `완료: ${summary.success}대`;
|
||||
document.getElementById('failed-count').textContent = `실패: ${summary.failed}대`;
|
||||
|
||||
|
||||
showMessage(`업로드 완료: 성공 ${summary.success}대, 실패 ${summary.failed}대`, 'success');
|
||||
showResults(data.results, '업로드 결과');
|
||||
|
||||
|
||||
refreshServers();
|
||||
});
|
||||
}
|
||||
@@ -412,9 +415,9 @@ function setupSocketIO() {
|
||||
function showResults(results, title) {
|
||||
const section = document.getElementById('result-section');
|
||||
const content = document.getElementById('result-content');
|
||||
|
||||
|
||||
section.style.display = 'block';
|
||||
|
||||
|
||||
content.innerHTML = `
|
||||
<h3>${title}</h3>
|
||||
<table class="result-table">
|
||||
@@ -448,30 +451,30 @@ function showResults(results, title) {
|
||||
|
||||
async function rebootSelected() {
|
||||
const serverIds = getSelectedServerIds();
|
||||
|
||||
|
||||
if (serverIds.length === 0) {
|
||||
showMessage('서버를 선택하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!confirm(`선택한 ${serverIds.length}대 서버를 재부팅하시겠습니까?\n\n⚠️ 업로드된 펌웨어가 적용됩니다!`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF('/idrac/api/servers/reboot-multi', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: JSON.stringify({
|
||||
server_ids: serverIds,
|
||||
type: 'GracefulRestart'
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
const summary = data.summary;
|
||||
showMessage(`재부팅 시작: 성공 ${summary.success}대, 실패 ${summary.failed}대`, 'success');
|
||||
@@ -479,7 +482,7 @@ async function rebootSelected() {
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
|
||||
|
||||
refreshServers();
|
||||
} catch (error) {
|
||||
showMessage('재부팅 실패: ' + error, 'error');
|
||||
@@ -499,11 +502,38 @@ function importExcel() {
|
||||
// ========================================
|
||||
|
||||
function showMessage(message, type = 'info') {
|
||||
// 간단한 알림 표시
|
||||
alert(message);
|
||||
// 토스트 알림으로 변경
|
||||
showToast(message, type);
|
||||
console.log(`[${type}] ${message}`);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 토스트 알림 시스템
|
||||
// ========================================
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
// 기존 토스트 제거
|
||||
const existingToast = document.querySelector('.toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
// 새 토스트 생성
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 애니메이션
|
||||
setTimeout(() => toast.classList.add('show'), 100);
|
||||
|
||||
// 3초 후 제거
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 편의 함수들
|
||||
function editServer(serverId) {
|
||||
showMessage('서버 수정 기능은 개발 중입니다', 'info');
|
||||
@@ -511,20 +541,20 @@ function editServer(serverId) {
|
||||
|
||||
function getSelectedFirmware() {
|
||||
const serverIds = getSelectedServerIds();
|
||||
|
||||
|
||||
if (serverIds.length === 0) {
|
||||
showMessage('서버를 선택하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
showMessage('선택한 서버의 펌웨어 조회 중...', 'info');
|
||||
|
||||
|
||||
// 각 서버별로 펌웨어 조회
|
||||
serverIds.forEach(async (serverId) => {
|
||||
try {
|
||||
const response = await fetchWithCSRF(`/idrac/api/servers/${serverId}/firmware`);
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
console.log(`Server ${serverId} firmware:`, data.data);
|
||||
}
|
||||
@@ -532,7 +562,7 @@ function getSelectedFirmware() {
|
||||
console.error(`Server ${serverId} firmware query failed:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 새로고침
|
||||
setTimeout(() => {
|
||||
refreshServers();
|
||||
@@ -580,11 +610,19 @@ async function compareSelectedFirmware() {
|
||||
const serverIds = getSelectedServerIds();
|
||||
|
||||
if (serverIds.length === 0) {
|
||||
showMessage('서버를 선택하세요', 'warning');
|
||||
showToast('서버를 선택하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage(`선택된 ${serverIds.length}대 서버 버전 비교 중...`, 'info');
|
||||
// 비교 탭으로 전환
|
||||
showTab('comparison');
|
||||
|
||||
// 로딩 표시
|
||||
const loadingEl = document.getElementById('comparison-loading');
|
||||
const resultEl = document.getElementById('comparison-result');
|
||||
|
||||
if (loadingEl) loadingEl.style.display = 'block';
|
||||
if (resultEl) resultEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetchWithCSRF('/idrac/api/servers/firmware/compare-multi', {
|
||||
@@ -594,16 +632,90 @@ async function compareSelectedFirmware() {
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// 로딩 숨기기
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
|
||||
if (data.success) {
|
||||
showResults(data.results, '버전 비교 결과');
|
||||
displayComparisonResults(data.results);
|
||||
showToast(`${serverIds.length}대 서버 비교 완료`, 'success');
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = `<div class="warning-text">⚠️ ${data.message}</div>`;
|
||||
}
|
||||
showToast(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('버전 비교 실패: ' + error, 'error');
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = `<div class="warning-text">⚠️ 버전 비교 실패: ${error}</div>`;
|
||||
}
|
||||
showToast('버전 비교 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 비교 결과 표시 (개선된 버전)
|
||||
// ========================================
|
||||
|
||||
function displayComparisonResults(results) {
|
||||
const resultEl = document.getElementById('comparison-result');
|
||||
if (!resultEl) return;
|
||||
|
||||
if (!results || results.length === 0) {
|
||||
resultEl.innerHTML = '<div class="info-text">비교 결과가 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="comparison-grid">';
|
||||
|
||||
results.forEach(serverResult => {
|
||||
html += `
|
||||
<div class="server-comparison-section">
|
||||
<h3 style="margin-bottom: 15px; color: #333;">
|
||||
🖥️ ${serverResult.server_name}
|
||||
<span style="font-size: 0.8em; color: #666;">(${serverResult.server_ip || ''})</span>
|
||||
</h3>
|
||||
`;
|
||||
|
||||
if (serverResult.success && serverResult.comparisons) {
|
||||
serverResult.comparisons.forEach(comp => {
|
||||
const statusClass = comp.status === 'outdated' ? 'outdated' :
|
||||
comp.status === 'latest' ? 'latest' : 'unknown';
|
||||
const statusIcon = comp.status === 'outdated' ? '⚠️' :
|
||||
comp.status === 'latest' ? '✅' : '❓';
|
||||
|
||||
html += `
|
||||
<div class="comparison-card ${statusClass}">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
|
||||
<strong style="font-size: 1.1em;">${comp.component_name}</strong>
|
||||
<span style="font-size: 1.5em;">${statusIcon}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span style="color: #666;">현재:</span>
|
||||
<code style="background: #f0f0f0; padding: 2px 8px; border-radius: 4px;">${comp.current_version}</code>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span style="color: #666;">최신:</span>
|
||||
<code style="background: #f0f0f0; padding: 2px 8px; border-radius: 4px;">${comp.latest_version || 'N/A'}</code>
|
||||
</div>
|
||||
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #ddd; font-size: 0.9em; color: #555;">
|
||||
${comp.recommendation}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += `<div class="warning-text">⚠️ ${serverResult.message || '비교 실패'}</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
resultEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 펌웨어 버전 추가 모달
|
||||
// ========================================
|
||||
@@ -681,7 +793,7 @@ async function refreshFirmwareVersionList() {
|
||||
// Dell Catalog에서 최신 버전 자동 가져오기
|
||||
// ========================================
|
||||
async function syncDellCatalog(model = "PowerEdge R750") {
|
||||
showMessage(`${model} 최신 버전 정보를 Dell에서 가져오는 중...`, "info");
|
||||
showToast(`${model} 최신 버전 정보를 Dell에서 가져오는 중...`, "info");
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF("/catalog/sync", {
|
||||
@@ -692,13 +804,95 @@ async function syncDellCatalog(model = "PowerEdge R750") {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showMessage(data.message, "success");
|
||||
showToast(data.message, "success");
|
||||
await refreshFirmwareVersionList();
|
||||
} else {
|
||||
showMessage(data.message, "error");
|
||||
showToast(data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage("버전 정보 동기화 실패: " + error, "error");
|
||||
showToast("버전 정보 동기화 실패: " + error, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DRM 리포지토리 동기화
|
||||
// ========================================
|
||||
|
||||
async function syncFromDRM() {
|
||||
// DRM 리포지토리 경로 입력 받기
|
||||
const repositoryPath = prompt(
|
||||
'DRM 리포지토리 경로를 입력하세요:\n예: C:\\Dell\\Repository 또는 \\\\network\\share\\DellRepo',
|
||||
'C:\\Dell\\Repository'
|
||||
);
|
||||
|
||||
if (!repositoryPath) return;
|
||||
|
||||
const model = prompt('서버 모델을 입력하세요:', 'PowerEdge R750');
|
||||
if (!model) return;
|
||||
|
||||
showToast('DRM 리포지토리에서 펌웨어 정보 가져오는 중...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF('/drm/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
repository_path: repositoryPath,
|
||||
model: model
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(`${data.message}`, 'success');
|
||||
await refreshFirmwareVersionList();
|
||||
} else {
|
||||
showToast(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('DRM 동기화 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDRMRepository() {
|
||||
const repositoryPath = prompt(
|
||||
'DRM 리포지토리 경로를 입력하세요:',
|
||||
'C:\\Dell\\Repository'
|
||||
);
|
||||
|
||||
if (!repositoryPath) return;
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF('/drm/check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ repository_path: repositoryPath })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.info) {
|
||||
const info = data.info;
|
||||
let message = `DRM 리포지토리 정보:\n\n`;
|
||||
message += `경로: ${info.path}\n`;
|
||||
message += `카탈로그 파일: ${info.catalog_file || '없음'}\n`;
|
||||
message += `총 패키지 수: ${info.total_packages || 0}\n`;
|
||||
|
||||
if (info.available_models && info.available_models.length > 0) {
|
||||
message += `\n사용 가능한 모델 (${info.available_models.length}개):\n`;
|
||||
message += info.available_models.slice(0, 10).join(', ');
|
||||
if (info.available_models.length > 10) {
|
||||
message += ` ... 외 ${info.available_models.length - 10}개`;
|
||||
}
|
||||
}
|
||||
|
||||
alert(message);
|
||||
} else {
|
||||
showToast('DRM 리포지토리를 찾을 수 없습니다', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('DRM 확인 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,8 +910,8 @@ async function refreshFirmwareVersionList() {
|
||||
|
||||
tbody.innerHTML = data.versions.length
|
||||
? data.versions
|
||||
.map(
|
||||
(v) => `
|
||||
.map(
|
||||
(v) => `
|
||||
<tr>
|
||||
<td>${v.component_name}</td>
|
||||
<td>${v.latest_version}</td>
|
||||
@@ -728,13 +922,79 @@ async function refreshFirmwareVersionList() {
|
||||
<button class="btn btn-danger" onclick="deleteFirmwareVersion(${v.id})">삭제</button>
|
||||
</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="6" class="empty-message">등록된 버전 정보가 없습니다</td></tr>`;
|
||||
} else {
|
||||
showMessage(data.message, "error");
|
||||
showToast(data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage("버전 목록 로드 실패: " + error, "error");
|
||||
showToast("버전 목록 로드 실패: " + error, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 탭 전환 기능
|
||||
// ========================================
|
||||
|
||||
function showTab(tabName) {
|
||||
// 모든 탭 콘텐츠 숨기기
|
||||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// 모든 탭 버튼 비활성화
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// 선택된 탭 표시
|
||||
const selectedTab = document.getElementById(tabName + '-tab');
|
||||
if (selectedTab) {
|
||||
selectedTab.classList.add('active');
|
||||
}
|
||||
|
||||
// 클릭된 버튼 활성화 (이벤트에서 호출된 경우)
|
||||
if (event && event.target) {
|
||||
event.target.classList.add('active');
|
||||
} else {
|
||||
// 직접 호출된 경우 해당 버튼 찾아서 활성화
|
||||
const buttons = document.querySelectorAll('.tab-button');
|
||||
buttons.forEach((btn, index) => {
|
||||
if ((tabName === 'servers' && index === 0) ||
|
||||
(tabName === 'versions' && index === 1) ||
|
||||
(tabName === 'comparison' && index === 2)) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 버전 탭 선택 시 목록 로드
|
||||
if (tabName === 'versions') {
|
||||
refreshFirmwareVersionList();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 펌웨어 버전 삭제
|
||||
// ========================================
|
||||
|
||||
async function deleteFirmwareVersion(versionId) {
|
||||
if (!confirm('이 펌웨어 버전 정보를 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF(`/idrac/api/firmware-versions/${versionId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
refreshFirmwareVersionList();
|
||||
} else {
|
||||
showToast(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('삭제 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user