update
This commit is contained in:
558
backend/static/css/idrac_style.css
Normal file
558
backend/static/css/idrac_style.css
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* Dell iDRAC 멀티 서버 펌웨어 관리 스타일
|
||||
* backend/static/css/idrac_style.css
|
||||
*/
|
||||
|
||||
/* 기본 스타일 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 헤더 */
|
||||
header {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #333;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
header .subtitle {
|
||||
color: #666;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* 카드 */
|
||||
.card {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
color: #333;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 필터 그룹 */
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 서버 테이블 */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.server-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.server-table thead {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.server-table th {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.server-table tbody tr {
|
||||
border-bottom: 1px solid #eee;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.server-table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.server-table td {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 상태 배지 */
|
||||
.status {
|
||||
padding: 5px 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-registered {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.status-updating {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #f0f0f0;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 버튼 */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #138496;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #e0a800;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 일괄 작업 */
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bulk-actions span {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* 모달 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 2em;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 20px 30px;
|
||||
border-top: 2px solid #eee;
|
||||
}
|
||||
|
||||
/* 폼 그룹 */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* 진행 상황 */
|
||||
.progress-item {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
transition: width 0.3s, background 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-uploading {
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.progress-completed {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.progress-failed {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-summary {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-stats span {
|
||||
padding: 5px 15px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 결과 테이블 */
|
||||
.result-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.result-table thead {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.result-table th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.result-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.result-table tr.success {
|
||||
background: #e8f5e9;
|
||||
}
|
||||
|
||||
.result-table tr.failed {
|
||||
background: #ffebee;
|
||||
}
|
||||
|
||||
.result-badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-failed {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 메시지 */
|
||||
.info-text {
|
||||
padding: 15px;
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
border-radius: 5px;
|
||||
color: #1976d2;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
padding: 15px;
|
||||
background: #fff3e0;
|
||||
border-left: 4px solid #ff9800;
|
||||
border-radius: 5px;
|
||||
color: #f57c00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 푸터 */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bulk-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 체크박스 커스텀 */
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
342
backend/static/css/version_compare_styles.css
Normal file
342
backend/static/css/version_compare_styles.css
Normal file
@@ -0,0 +1,342 @@
|
||||
/* 탭 메뉴 스타일 */
|
||||
.tab-menu {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 12px 25px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 버전 상태 표시 */
|
||||
.version-status {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-status-outdated {
|
||||
background: #fee;
|
||||
color: #dc3545;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
.version-status-outdated::before {
|
||||
content: "🔴 ";
|
||||
}
|
||||
|
||||
.version-status-latest {
|
||||
background: #efe;
|
||||
color: #28a745;
|
||||
border: 1px solid #cfc;
|
||||
}
|
||||
|
||||
.version-status-latest::before {
|
||||
content: "🟢 ";
|
||||
}
|
||||
|
||||
.version-status-unknown {
|
||||
background: #ffeaa7;
|
||||
color: #e17055;
|
||||
border: 1px solid #fdcb6e;
|
||||
}
|
||||
|
||||
.version-status-unknown::before {
|
||||
content: "🟡 ";
|
||||
}
|
||||
|
||||
/* 버전 비교 결과 카드 */
|
||||
.comparison-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.comparison-card h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.comparison-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-item .label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.summary-item .value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summary-item.outdated .value {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.summary-item.latest .value {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.summary-item.unknown .value {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
/* 펌웨어 항목 리스트 */
|
||||
.firmware-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.firmware-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.firmware-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.firmware-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.firmware-versions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.version-current {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.version-arrow {
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.version-latest {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-recommendation {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* 중요 업데이트 배지 */
|
||||
.critical-badge {
|
||||
display: inline-block;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 다운로드 링크 */
|
||||
.download-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.download-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 버전 관리 테이블 */
|
||||
.version-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.version-table th {
|
||||
background: #f8f9fa;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.version-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.version-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 빈 상태 메시지 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state .icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-state .message {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 필터 그룹 개선 */
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 진행률 표시 */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.tab-menu {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
border-bottom: none;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
border-left-color: #667eea;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.comparison-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.firmware-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.firmware-versions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
740
backend/static/js/idrac_main.js
Normal file
740
backend/static/js/idrac_main.js
Normal file
@@ -0,0 +1,740 @@
|
||||
/**
|
||||
* Dell iDRAC 멀티 서버 펌웨어 관리 JavaScript
|
||||
* backend/static/js/idrac_main.js
|
||||
*/
|
||||
|
||||
// SocketIO 연결
|
||||
const socket = io();
|
||||
|
||||
// 전역 변수
|
||||
let servers = [];
|
||||
let selectedServers = new Set();
|
||||
|
||||
// CSRF 토큰 가져오기
|
||||
function getCSRFToken() {
|
||||
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
||||
if (metaTag) return metaTag.getAttribute('content');
|
||||
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'csrf_token') return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// fetch 래퍼 함수
|
||||
async function fetchWithCSRF(url, options = {}) {
|
||||
const csrfToken = getCSRFToken();
|
||||
if (!options.headers) options.headers = {};
|
||||
|
||||
if (csrfToken) {
|
||||
options.headers['X-CSRFToken'] = csrfToken;
|
||||
options.headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (options.body instanceof FormData) {
|
||||
// 자동 처리
|
||||
} else if (typeof options.body === 'string') {
|
||||
if (!options.headers['Content-Type']) {
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
} else if (typeof options.body === 'object') {
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 초기화
|
||||
// ========================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
console.log('iDRAC 멀티 서버 관리 시스템 시작');
|
||||
refreshServers();
|
||||
loadGroups();
|
||||
setupSocketIO();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// 서버 관리
|
||||
// ========================================
|
||||
|
||||
async function refreshServers() {
|
||||
try {
|
||||
const group = document.getElementById('group-filter').value;
|
||||
const url = `/idrac/api/servers${group !== 'all' ? '?group=' + group : ''}`;
|
||||
const response = await fetchWithCSRF(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
servers = data.servers;
|
||||
renderServerList();
|
||||
updateServerCount();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('서버 목록 로드 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderServerList() {
|
||||
const tbody = document.getElementById('server-list');
|
||||
if (servers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-message">등록된 서버가 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = servers.map(server => `
|
||||
<tr data-server-id="${server.id}">
|
||||
<td><input type="checkbox" class="server-checkbox" value="${server.id}"
|
||||
${selectedServers.has(server.id) ? 'checked' : ''}
|
||||
onchange="toggleServerSelection(${server.id})"></td>
|
||||
<td><strong>${server.name}</strong></td>
|
||||
<td><code>${server.ip_address}</code></td>
|
||||
<td><span class="badge">${server.group_name || '-'}</span></td>
|
||||
<td><span class="status status-${server.status}">${getStatusText(server.status)}</span></td>
|
||||
<td>${server.current_bios || '-'}</td>
|
||||
<td>${server.last_connected ? formatDateTime(server.last_connected) : '-'}</td>
|
||||
<td class="action-buttons">
|
||||
<button onclick="testConnection(${server.id})" class="btn-icon" title="연결 테스트">🔌</button>
|
||||
<button onclick="editServer(${server.id})" class="btn-icon" title="수정">✏️</button>
|
||||
<button onclick="deleteServer(${server.id})" class="btn-icon" title="삭제">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
const map = { registered: '등록됨', online: '온라인', offline: '오프라인', updating: '업데이트중' };
|
||||
return map[status] || status;
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('ko-KR', {
|
||||
year: '2-digit', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function updateServerCount() {
|
||||
document.getElementById('server-count').textContent = `${servers.length}대`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 서버 선택
|
||||
// ========================================
|
||||
|
||||
function toggleSelectAll() {
|
||||
const checkbox = document.getElementById('select-all');
|
||||
const checkboxes = document.querySelectorAll('.server-checkbox');
|
||||
selectedServers.clear();
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = checkbox.checked;
|
||||
if (checkbox.checked) selectedServers.add(parseInt(cb.value));
|
||||
});
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function toggleServerSelection(serverId) {
|
||||
if (selectedServers.has(serverId)) selectedServers.delete(serverId);
|
||||
else selectedServers.add(serverId);
|
||||
updateSelectedCount();
|
||||
|
||||
const all = document.querySelectorAll('.server-checkbox');
|
||||
const checked = document.querySelectorAll('.server-checkbox:checked');
|
||||
document.getElementById('select-all').checked = all.length === checked.length;
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
document.getElementById('selected-count').textContent = `선택: ${selectedServers.size}대`;
|
||||
}
|
||||
|
||||
function getSelectedServerIds() {
|
||||
return Array.from(selectedServers);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 그룹 관리
|
||||
// ========================================
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const response = await fetchWithCSRF('/idrac/api/groups');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const select = document.getElementById('group-filter');
|
||||
const currentValue = select.value;
|
||||
select.innerHTML = '<option value="all">전체</option>';
|
||||
data.groups.forEach(g => select.innerHTML += `<option value="${g}">${g}</option>`);
|
||||
if (currentValue) select.value = currentValue;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('그룹 로드 실패:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function filterByGroup() {
|
||||
refreshServers();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 서버 추가/수정/삭제
|
||||
// ========================================
|
||||
|
||||
function showAddServerModal() {
|
||||
document.getElementById('add-server-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAddServerModal() {
|
||||
const modal = document.getElementById('add-server-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
setTimeout(() => clearServerForm(), 0); // DOM 안정 후 폼 초기화
|
||||
}
|
||||
|
||||
function clearServerForm() {
|
||||
const fieldIds = [
|
||||
'server-name', 'server-ip', 'server-username',
|
||||
'server-password', 'server-group', 'server-model'
|
||||
];
|
||||
fieldIds.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
if (el.type === 'checkbox') el.checked = false;
|
||||
else if (el.type === 'file') el.value = null;
|
||||
else el.value = '';
|
||||
} else {
|
||||
console.warn(`[clearServerForm] 요소 없음: #${id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function addServer() {
|
||||
const serverData = {
|
||||
name: document.getElementById('server-name').value,
|
||||
ip_address: document.getElementById('server-ip').value,
|
||||
username: document.getElementById('server-username').value,
|
||||
password: document.getElementById('server-password').value,
|
||||
group_name: document.getElementById('server-group').value,
|
||||
model: document.getElementById('server-model').value
|
||||
};
|
||||
|
||||
if (!serverData.name || !serverData.ip_address || !serverData.password) {
|
||||
showMessage('필수 필드를 입력하세요 (서버명, IP, 비밀번호)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF('/idrac/api/servers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(serverData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
closeAddServerModal();
|
||||
refreshServers();
|
||||
loadGroups();
|
||||
} else showMessage(data.message, 'error');
|
||||
} catch (e) {
|
||||
showMessage('서버 추가 실패: ' + e, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteServer(serverId) {
|
||||
if (!confirm('이 서버를 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const res = await fetchWithCSRF(`/idrac/api/servers/${serverId}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showMessage(data.message, 'success');
|
||||
refreshServers();
|
||||
} else showMessage(data.message, 'error');
|
||||
} catch (e) {
|
||||
showMessage('서버 삭제 실패: ' + e, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 연결 테스트
|
||||
// ========================================
|
||||
|
||||
async function testConnection(serverId) {
|
||||
try {
|
||||
const res = await fetchWithCSRF(`/idrac/api/servers/${serverId}/test`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
refreshServers();
|
||||
} catch (e) {
|
||||
showMessage('연결 테스트 실패: ' + e, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 펌웨어 업로드
|
||||
// ========================================
|
||||
|
||||
function showUploadModal() {
|
||||
const ids = getSelectedServerIds();
|
||||
if (ids.length === 0) return showMessage('서버를 선택하세요', 'warning');
|
||||
document.getElementById('upload-server-count').textContent = `${ids.length}대`;
|
||||
document.getElementById('upload-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeUploadModal() {
|
||||
document.getElementById('upload-modal').style.display = 'none';
|
||||
document.getElementById('firmware-file').value = '';
|
||||
}
|
||||
|
||||
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 {
|
||||
showMessage(data.message, 'error');
|
||||
hideUploadProgress();
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('업로드 시작 실패: ' + error, 'error');
|
||||
hideUploadProgress();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return `
|
||||
<div class="progress-item" id="progress-${id}">
|
||||
<div class="progress-header">
|
||||
<strong>${server.name}</strong> (${server.ip_address})
|
||||
<span class="progress-status" id="status-${id}">대기중...</span>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="bar-${id}" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-message" id="message-${id}"></div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 요약 초기화
|
||||
document.getElementById('progress-summary').innerHTML = `
|
||||
<div class="summary-stats">
|
||||
<span>전체: ${serverIds.length}대</span>
|
||||
<span id="completed-count">완료: 0대</span>
|
||||
<span id="failed-count">실패: 0대</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function hideUploadProgress() {
|
||||
document.getElementById('upload-progress-section').style.display = 'none';
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SocketIO 이벤트
|
||||
// ========================================
|
||||
|
||||
function setupSocketIO() {
|
||||
// 업로드 진행 상황
|
||||
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 + '%';
|
||||
barEl.className = `progress-bar progress-${data.status}`;
|
||||
}
|
||||
if (messageEl) messageEl.textContent = data.job_id ? `Job ID: ${data.job_id}` : '';
|
||||
});
|
||||
|
||||
// 업로드 완료
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 결과 표시
|
||||
// ========================================
|
||||
|
||||
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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>서버명</th>
|
||||
<th>상태</th>
|
||||
<th>메시지</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.map(r => `
|
||||
<tr class="${r.success ? 'success' : 'failed'}">
|
||||
<td><strong>${r.server_name}</strong></td>
|
||||
<td>
|
||||
<span class="result-badge ${r.success ? 'badge-success' : 'badge-failed'}">
|
||||
${r.success ? '✓ 성공' : '✗ 실패'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${r.message}${r.job_id ? ` (Job: ${r.job_id})` : ''}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 재부팅
|
||||
// ========================================
|
||||
|
||||
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({
|
||||
server_ids: serverIds,
|
||||
type: 'GracefulRestart'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const summary = data.summary;
|
||||
showMessage(`재부팅 시작: 성공 ${summary.success}대, 실패 ${summary.failed}대`, 'success');
|
||||
showResults(data.results, '재부팅 결과');
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
|
||||
refreshServers();
|
||||
} catch (error) {
|
||||
showMessage('재부팅 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Excel 가져오기 (추후 구현)
|
||||
// ========================================
|
||||
|
||||
function importExcel() {
|
||||
showMessage('Excel 가져오기 기능은 개발 중입니다', 'info');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 유틸리티
|
||||
// ========================================
|
||||
|
||||
function showMessage(message, type = 'info') {
|
||||
// 간단한 알림 표시
|
||||
alert(message);
|
||||
console.log(`[${type}] ${message}`);
|
||||
}
|
||||
|
||||
// 편의 함수들
|
||||
function editServer(serverId) {
|
||||
showMessage('서버 수정 기능은 개발 중입니다', 'info');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Server ${serverId} firmware query failed:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// 새로고침
|
||||
setTimeout(() => {
|
||||
refreshServers();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 다중 연결 테스트
|
||||
// ========================================
|
||||
async function testSelectedConnections() {
|
||||
const serverIds = getSelectedServerIds();
|
||||
|
||||
if (serverIds.length === 0) {
|
||||
showMessage('서버를 선택하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage(`선택된 ${serverIds.length}대 서버 연결 테스트 중...`, 'info');
|
||||
|
||||
try {
|
||||
const res = await fetchWithCSRF('/idrac/api/servers/test-multi', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ server_ids: serverIds })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
const summary = data.summary;
|
||||
showMessage(`연결 성공: ${summary.success}대 / 실패: ${summary.failed}대`, 'success');
|
||||
showResults(data.results, '연결 테스트 결과');
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
refreshServers();
|
||||
} catch (error) {
|
||||
showMessage('연결 테스트 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 다중 펌웨어 버전 비교
|
||||
// ========================================
|
||||
async function compareSelectedFirmware() {
|
||||
const serverIds = getSelectedServerIds();
|
||||
|
||||
if (serverIds.length === 0) {
|
||||
showMessage('서버를 선택하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage(`선택된 ${serverIds.length}대 서버 버전 비교 중...`, 'info');
|
||||
|
||||
try {
|
||||
const res = await fetchWithCSRF('/idrac/api/servers/firmware/compare-multi', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ server_ids: serverIds })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showResults(data.results, '버전 비교 결과');
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('버전 비교 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 펌웨어 버전 추가 모달
|
||||
// ========================================
|
||||
function showAddVersionModal() {
|
||||
const modal = document.getElementById('add-version-modal');
|
||||
if (modal) modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAddVersionModal() {
|
||||
const modal = document.getElementById('add-version-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
async function addFirmwareVersion() {
|
||||
const data = {
|
||||
component_name: document.getElementById('version-component').value,
|
||||
latest_version: document.getElementById('version-latest').value,
|
||||
server_model: document.getElementById('version-model').value,
|
||||
vendor: document.getElementById('version-vendor').value,
|
||||
release_date: document.getElementById('version-release-date').value,
|
||||
download_url: document.getElementById('version-download-url').value,
|
||||
notes: document.getElementById('version-notes').value,
|
||||
is_critical: document.getElementById('version-critical').checked
|
||||
};
|
||||
|
||||
if (!data.component_name || !data.latest_version) {
|
||||
showMessage('컴포넌트명과 버전을 입력하세요', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF('/idrac/api/firmware-versions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showMessage(result.message, 'success');
|
||||
closeAddVersionModal();
|
||||
refreshFirmwareVersionList();
|
||||
} else {
|
||||
showMessage(result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('버전 추가 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFirmwareVersionList() {
|
||||
try {
|
||||
const response = await fetchWithCSRF('/idrac/api/firmware-versions');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const tbody = document.getElementById('version-list');
|
||||
tbody.innerHTML = data.versions.map(v => `
|
||||
<tr>
|
||||
<td>${v.component_name}</td>
|
||||
<td>${v.latest_version}</td>
|
||||
<td>${v.server_model || '-'}</td>
|
||||
<td>${v.release_date || '-'}</td>
|
||||
<td>${v.is_critical ? '⚠️' : ''}</td>
|
||||
<td><button class="btn btn-danger" onclick="deleteFirmwareVersion(${v.id})">삭제</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('버전 목록 로드 실패: ' + error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Dell Catalog에서 최신 버전 자동 가져오기
|
||||
// ========================================
|
||||
async function syncDellCatalog(model = "PowerEdge R750") {
|
||||
showMessage(`${model} 최신 버전 정보를 Dell에서 가져오는 중...`, "info");
|
||||
|
||||
try {
|
||||
const response = await fetchWithCSRF("/catalog/sync", {
|
||||
method: "POST",
|
||||
body: { model }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showMessage(data.message, "success");
|
||||
await refreshFirmwareVersionList();
|
||||
} else {
|
||||
showMessage(data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage("버전 정보 동기화 실패: " + error, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 펌웨어 버전 목록 새로고침
|
||||
// ========================================
|
||||
async function refreshFirmwareVersionList() {
|
||||
try {
|
||||
const response = await fetchWithCSRF("/idrac/api/firmware-versions");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const tbody = document.getElementById("version-list");
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = data.versions.length
|
||||
? data.versions
|
||||
.map(
|
||||
(v) => `
|
||||
<tr>
|
||||
<td>${v.component_name}</td>
|
||||
<td>${v.latest_version}</td>
|
||||
<td>${v.server_model || "-"}</td>
|
||||
<td>${v.release_date || "-"}</td>
|
||||
<td>${v.is_critical ? "⚠️" : ""}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger" onclick="deleteFirmwareVersion(${v.id})">삭제</button>
|
||||
</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("")
|
||||
: `<tr><td colspan="6" class="empty-message">등록된 버전 정보가 없습니다</td></tr>`;
|
||||
} else {
|
||||
showMessage(data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage("버전 목록 로드 실패: " + error, "error");
|
||||
}
|
||||
}
|
||||
283
backend/static/style - 복사본.css
Normal file
283
backend/static/style - 복사본.css
Normal file
@@ -0,0 +1,283 @@
|
||||
/* style.css */
|
||||
|
||||
/* 기본 스타일 재정의 */
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px; /* 파일 목록 아래 여백 추가 */
|
||||
}
|
||||
|
||||
.simple-card {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
width: calc(12.5% - 10px); /* 가로로 8개 유지 */
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease; /* 부드러운 전환 효과 추가 */
|
||||
}
|
||||
|
||||
.simple-card a {
|
||||
font-size: 1em; /* 글씨 크기를 작게 조정 */
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.simple-card a:hover {
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.simple-card:hover {
|
||||
background-color: #e9ecef; /* 호버 시 배경색 변경 */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.backup-card-container .backup-card {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.accordion-button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accordion-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
display: none;
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 20px; /* 파일 목록 아래 여백 추가 */
|
||||
}
|
||||
|
||||
.accordion-content .card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.accordion-content .simple-card {
|
||||
width: calc(12.5% - 10px); /* 가로로 8개 유지 */
|
||||
}
|
||||
|
||||
/* 진행 상황 스타일 */
|
||||
#progressSession {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin-bottom: 20px; /* 파일 목록 아래 여백 추가 */
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 25px;
|
||||
border-radius: 5px;
|
||||
background-color: #f4f4f4;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
font-weight: bold;
|
||||
line-height: 25px;
|
||||
background-color: #007bff;
|
||||
border-radius: 5px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
#progressSession h2 {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 15px;
|
||||
background-color: #e9ecef;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 1.2em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 서버 리스트 박스 높이 조정 */
|
||||
#server_list_content {
|
||||
height: 340px; /* 원하는 높이로 설정 */
|
||||
}
|
||||
|
||||
/* base */
|
||||
|
||||
.navbar {
|
||||
background-color: #333;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.nav-list li a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-list li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 58px; /* 상단 네비게이션 바의 높이 */
|
||||
}
|
||||
|
||||
/* XML 파일 목록의 스타일 정의 */
|
||||
.list-group-item {
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 마우스 오버 시 배경색 및 효과 변경 */
|
||||
.list-group-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 삭제 및 편집 버튼 간격 조정 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 파일 목록 타이틀 스타일 정의 */
|
||||
.card-body h5 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 파일 업로드 필드의 스타일 */
|
||||
.custom-file-label {
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background-color: #f1f1f1;
|
||||
border: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
white-space: nowrap; /* 파일 이름이 한 줄로 보이게 함 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* 파일 이름이 길 경우 생략 표시 */
|
||||
}
|
||||
|
||||
.custom-file-label:hover {
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
|
||||
/* 업로드 버튼 스타일 */
|
||||
.btn-upload {
|
||||
margin-top: 10px;
|
||||
width: auto; /* 버튼 너비를 텍스트에 맞게 조정 */
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* XML 파일 업로드 필드 레이아웃 조정 */
|
||||
.upload-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user