2030 lines
91 KiB
HTML
2030 lines
91 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<script>
|
|
var savedTheme = localStorage.getItem('audit_theme');
|
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
}
|
|
</script>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Kết quả xử lý - VNPT</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: var(--bg-main);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background:
|
|
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.05) 48%, rgba(33, 150, 243, 0.05) 52%, transparent 52%, transparent 100%),
|
|
linear-gradient(45deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.08) 48%, rgba(33, 150, 243, 0.08) 52%, transparent 52%, transparent 100%),
|
|
linear-gradient(135deg, transparent 0%, transparent 48%, rgba(33, 150, 243, 0.03) 48%, rgba(33, 150, 243, 0.03) 52%, transparent 52%, transparent 100%);
|
|
background-size: 300px 300px, 400px 400px, 500px 500px;
|
|
background-position: 0 0, 100px 100px, 200px 0;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
body::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 0;
|
|
height: 0;
|
|
border-style: solid;
|
|
border-width: 0 400px 400px 0;
|
|
border-color: transparent rgba(33, 150, 243, 0.03) transparent transparent;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.container {
|
|
background: var(--card-bg);
|
|
border-radius: 20px;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
|
padding: 40px;
|
|
max-width: 1100px;
|
|
width: 100%;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
h1 {
|
|
color: var(--text-main);
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
font-size: 28px;
|
|
}
|
|
|
|
.success-icon {
|
|
text-align: center;
|
|
font-size: 60px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
margin-bottom: 30px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.file-list {
|
|
list-style: none;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.file-item {
|
|
background: var(--table-header-bg);
|
|
padding: 15px 20px;
|
|
margin-bottom: 10px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: all 0.3s ease;
|
|
border: 2px solid transparent;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: var(--hover-bg);
|
|
border-color: var(--primary-color);
|
|
transform: translateX(5px);
|
|
}
|
|
|
|
.file-name {
|
|
color: var(--text-main);
|
|
font-weight: 500;
|
|
flex: 1;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.file-icon {
|
|
margin-right: 10px;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.btn-download {
|
|
padding: 8px 20px;
|
|
background: #1976d2;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: background 0.3s ease;
|
|
display: inline-block;
|
|
}
|
|
|
|
.btn-download:hover {
|
|
background: #1565c0;
|
|
}
|
|
|
|
.btn-back {
|
|
width: 100%;
|
|
padding: 15px;
|
|
background: #6c757d;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 10px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
display: block;
|
|
margin-top: 30px;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.btn-back:hover {
|
|
background: #5a6268;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 50px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.footer {
|
|
text-align: center;
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #eee;
|
|
color: var(--text-muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.btn-secondary {
|
|
flex: 1;
|
|
padding: 12px;
|
|
background: #6c757d;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
display: inline-block;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #5a6268;
|
|
}
|
|
|
|
.btn-primary {
|
|
flex: 1;
|
|
padding: 12px;
|
|
background: #1976d2;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
display: inline-block;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #1565c0;
|
|
}
|
|
|
|
/* Download notification */
|
|
.download-notification {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
|
color: white;
|
|
padding: 15px 25px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
display: none;
|
|
align-items: center;
|
|
gap: 10px;
|
|
animation: slideIn 0.3s ease-out;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.download-notification.show {
|
|
display: flex;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.download-spinner {
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
border-top-color: white;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
/* Responsive Styles */
|
|
@media screen and (max-width: 768px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.container {
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 22px;
|
|
}
|
|
|
|
.success-icon {
|
|
font-size: 45px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.file-item {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
text-align: center;
|
|
padding: 12px 15px;
|
|
}
|
|
|
|
.file-item>div {
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-download {
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.action-buttons {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn-secondary,
|
|
.btn-primary {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 480px) {
|
|
body {
|
|
padding: 5px;
|
|
}
|
|
|
|
.container {
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.success-icon {
|
|
font-size: 35px;
|
|
}
|
|
|
|
.file-name {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.btn-download,
|
|
.btn-secondary,
|
|
.btn-primary,
|
|
.btn-back {
|
|
font-size: 13px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.footer {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.download-notification {
|
|
bottom: 10px;
|
|
right: 10px;
|
|
left: 10px;
|
|
padding: 10px 15px;
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
|
|
/* Collapsible details styling */
|
|
details summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
details summary::marker {
|
|
display: none;
|
|
content: '';
|
|
}
|
|
|
|
details summary:hover {
|
|
background: var(--hover-bg) !important;
|
|
}
|
|
|
|
details[open] summary {
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.collapse-arrow {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
/* Download all button hover */
|
|
#btnDownloadAll:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.4) !important;
|
|
}
|
|
|
|
#btnExportCSV:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.4) !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
{% include 'header.html' %}
|
|
<!-- Download Notification -->
|
|
<div class="download-notification" id="downloadNotification">
|
|
<div class="download-spinner"></div>
|
|
<span id="downloadText">Đang tải file...</span>
|
|
</div>
|
|
<div class="container">
|
|
<!-- Top Navigation Tabs -->
|
|
<div class="top-nav-tabs"
|
|
style="display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; background: rgba(25, 118, 210, 0.05); padding: 15px; border-radius: 12px; border: 1px solid rgba(25, 118, 210, 0.1);">
|
|
<a href="{{ url_for('index') }}" class="nav-tab"
|
|
style="padding: 10px 20px; background: var(--card-bg); color: var(--text-main); text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: 1px solid var(--border-color); box-shadow: 0 2px 5px rgba(0,0,0,0.05);">🏠 Trang chủ</a>
|
|
<a href="{{ url_for('my_files') }}" class="nav-tab"
|
|
style="padding: 10px 20px; background: {% if session_mode %}linear-gradient(135deg, #1976d2 0%, #1565c0 100%){% else %}var(--card-bg){% endif %}; color: {% if session_mode %}white{% else %}var(--text-main){% endif %}; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: {% if not session_mode %}1px solid var(--border-color){% else %}none{% endif %}; box-shadow: {% if session_mode %}0 4px 10px rgba(25, 118, 210, 0.3){% else %}0 2px 5px rgba(0,0,0,0.05){% endif %};">📁 File của tôi</a>
|
|
{% if user_info and user_info.role == 'admin' %}
|
|
<a href="{{ url_for('admin_tools') }}" class="nav-tab"
|
|
style="padding: 10px 20px; background: {% if not session_mode %}linear-gradient(135deg, #1976d2 0%, #1565c0 100%){% else %}var(--card-bg){% endif %}; color: {% if not session_mode %}white{% else %}var(--text-main){% endif %}; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 14px; transition: all 0.3s ease; border: {% if session_mode %}1px solid var(--border-color){% else %}none{% endif %}; box-shadow: {% if not session_mode %}0 4px 10px rgba(25, 118, 210, 0.3){% else %}0 2px 5px rgba(0,0,0,0.05){% endif %};">⚙️ Quản trị</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if not session_mode and page_name == 'admin' %}
|
|
<!-- Admin Sub Navigation Tabs -->
|
|
<div class="admin-sub-tabs"
|
|
style="display: flex; justify-content: center; gap: 8px; margin-bottom: 25px; flex-wrap: wrap;">
|
|
<a href="{{ url_for('admin_tools') }}" class="sub-tab"
|
|
style="padding: 8px 16px; background-color: var(--info-bg); color: var(--primary-color); text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; border: 1px solid #bbdefb;">🔧 Cài đặt chung</a>
|
|
<a href="{{ url_for('admin_users_page') }}" class="sub-tab"
|
|
style="padding: 8px 16px; background-color: var(--info-bg); color: var(--primary-color); text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; border: 1px solid #bbdefb;">👥 Người dùng & Đơn vị</a>
|
|
<a href="{{ url_for('list_outputs') }}" class="sub-tab"
|
|
style="padding: 8px 16px; background: #1565c0; color: white; text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(21, 101, 192, 0.3);">📂 Tất cả File</a>
|
|
<a href="{{ url_for('server_stats_page') }}" class="sub-tab"
|
|
style="padding: 8px 16px; background-color: var(--info-bg); color: var(--primary-color); text-decoration: none; border-radius: 20px; font-weight: 600; font-size: 13px; transition: all 0.3s ease; border: 1px solid #bbdefb;">📊 Thống kê</a>
|
|
</div>
|
|
{% endif %}
|
|
{% if message %}
|
|
<div
|
|
style="margin-bottom: 20px; padding: 15px; border-radius: 10px; background: {% if message.type == 'success' %}#d4edda{% elif message.type == 'error' %}#f8d7da{% elif message.type == 'info' %}#e7f3ff{% else %}#fff3cd{% endif %}; color: {% if message.type == 'success' %}#155724{% elif message.type == 'error' %}#721c24{% elif message.type == 'info' %}#004085{% else %}#856404{% endif %}; border: 1px solid {% if message.type == 'success' %}#c3e6cb{% elif message.type == 'error' %}#f5c6cb{% elif message.type == 'info' %}#b8daff{% else %}#ffeaa7{% endif %};">
|
|
{{ message.text }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if (show_report_builder or session_mode) and files_info %}
|
|
<!-- ===== ADMIN REPORT BUILDER ===== -->
|
|
<style>
|
|
.report-builder {
|
|
margin: 25px 0;
|
|
}
|
|
|
|
.rb-card {
|
|
background: var(--card-bg);
|
|
border-radius: 15px;
|
|
padding: 25px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.rb-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 2px solid var(--border-color);
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.rb-title {
|
|
font-size: 20px;
|
|
color: var(--primary-color);
|
|
font-weight: 700;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.rb-search {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
width: 100%;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.rb-search-input {
|
|
flex: 1;
|
|
min-width: 250px;
|
|
padding: 12px 15px 12px 40px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--input-border);
|
|
background: var(--input-bg);
|
|
color: var(--input-text);
|
|
font-size: 14px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.rb-search-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
background: var(--input-bg);
|
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15);
|
|
}
|
|
|
|
.rb-search-wrapper {
|
|
flex: 1;
|
|
min-width: 250px;
|
|
position: relative;
|
|
}
|
|
|
|
.rb-search-wrapper::before {
|
|
content: '🔍';
|
|
position: absolute;
|
|
left: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 16px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.rb-stats {
|
|
display: flex;
|
|
gap: 15px;
|
|
align-items: center;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.rb-stat-badge {
|
|
background-color: var(--info-bg);
|
|
color: var(--primary-color);
|
|
padding: 3px 10px;
|
|
border-radius: 12px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.rb-stat-selected {
|
|
background-color: var(--success-bg);
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.rb-action-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding: 12px 16px;
|
|
background: var(--bg-main);
|
|
border-radius: 10px;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.rb-action-left,
|
|
.rb-action-right {
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.rb-checkbox {
|
|
appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 2px solid #bbb;
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.2s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.rb-checkbox:checked {
|
|
background: #1976d2;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.rb-checkbox:checked::after {
|
|
content: '✓';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.rb-checkbox:hover {
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.rb-btn-generate {
|
|
padding: 10px 20px;
|
|
background: linear-gradient(135deg, #2e7d32, #43a047);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
box-shadow: 0 3px 12px rgba(46, 125, 50, 0.3);
|
|
}
|
|
|
|
.rb-btn-generate:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 20px rgba(46, 125, 50, 0.4);
|
|
}
|
|
|
|
.rb-btn-generate:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.rb-table-container {
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.rb-table-container::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.rb-table-container::-webkit-scrollbar-track {
|
|
background: var(--bg-main);
|
|
}
|
|
|
|
.rb-table-container::-webkit-scrollbar-thumb {
|
|
background-color: var(--border-color);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.rb-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.rb-table th {
|
|
padding: 12px 14px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
text-transform: uppercase;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, #1565c0, #1976d2);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 2;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.rb-table td {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
color: var(--text-main);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.rb-table tbody tr {
|
|
transition: background 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.rb-table tbody tr:nth-child(odd) {
|
|
background: var(--card-bg);
|
|
}
|
|
|
|
.rb-table tbody tr:nth-child(even) {
|
|
background: var(--bg-main);
|
|
}
|
|
|
|
.rb-table tbody tr:hover {
|
|
background-color: var(--hover-bg);
|
|
}
|
|
|
|
.rb-table tbody tr.rb-selected {
|
|
background: var(--success-bg) !important;
|
|
}
|
|
|
|
.rb-hostname {
|
|
font-weight: 600;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.rb-os {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.rb-ip {
|
|
font-family: 'Consolas', 'Courier New', monospace;
|
|
color: var(--text-main);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.rb-pct-badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 15px;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
min-width: 55px;
|
|
}
|
|
|
|
.rb-pct-green {
|
|
background-color: var(--success-bg);
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.rb-pct-orange {
|
|
background-color: var(--warning-bg);
|
|
color: var(--warning-color);
|
|
}
|
|
|
|
.rb-pct-red {
|
|
background-color: var(--danger-bg);
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
.rb-mand-ok {
|
|
color: var(--success-color);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.rb-mand-warn {
|
|
color: var(--warning-color);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.rb-mand-fail {
|
|
color: var(--danger-color);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.rb-date {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.rb-dl-btn {
|
|
padding: 5px 12px;
|
|
background: linear-gradient(135deg, #1976d2, #1565c0);
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
display: inline-block;
|
|
}
|
|
|
|
.rb-dl-btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 3px 10px rgba(25, 118, 210, 0.3);
|
|
}
|
|
|
|
.rb-dl-btn-excel {
|
|
padding: 5px 10px;
|
|
background: linear-gradient(135deg, #2e7d32, #1b5e20);
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
transition: all 0.25s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
box-shadow: 0 2px 5px rgba(46, 125, 50, 0.2);
|
|
}
|
|
|
|
.rb-dl-btn-excel:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 10px rgba(46, 125, 50, 0.35);
|
|
background: linear-gradient(135deg, #388e3c, #2e7d32);
|
|
}
|
|
|
|
.rb-dl-btn-text {
|
|
padding: 5px 10px;
|
|
background: linear-gradient(135deg, #1976d2, #0d47a1);
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
transition: all 0.25s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
box-shadow: 0 2px 5px rgba(25, 118, 210, 0.2);
|
|
}
|
|
|
|
.rb-dl-btn-text:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 10px rgba(25, 118, 210, 0.35);
|
|
background: linear-gradient(135deg, #1e88e5, #1565c0);
|
|
}
|
|
|
|
.rb-toast {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
padding: 15px 25px;
|
|
border-radius: 12px;
|
|
color: white;
|
|
font-weight: 500;
|
|
z-index: 1000;
|
|
transform: translateY(100px);
|
|
opacity: 0;
|
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.rb-toast.show {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
.rb-toast-success {
|
|
background: linear-gradient(135deg, #2e7d32, #43a047);
|
|
}
|
|
|
|
.rb-toast-error {
|
|
background: linear-gradient(135deg, #c62828, #e53935);
|
|
}
|
|
|
|
.rb-toast-info {
|
|
background: linear-gradient(135deg, #1565c0, #1976d2);
|
|
}
|
|
|
|
.rb-empty-filter {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: var(--text-muted);
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.rb-search {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.rb-action-bar {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.rb-table {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.rb-table th,
|
|
.rb-table td {
|
|
padding: 8px 10px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="report-builder">
|
|
<div class="rb-card">
|
|
<div class="rb-header">
|
|
<h2 class="rb-title">{% if session_mode %}📁 File của tôi{% else %}📊 Tất cả File kết quả{% endif %}</h2>
|
|
</div>
|
|
|
|
<!-- Search & Stats -->
|
|
<div class="rb-search">
|
|
<div class="rb-search-wrapper">
|
|
<input type="text" id="rbSearchInput" class="rb-search-input"
|
|
placeholder="Tìm theo tên máy chủ, địa chỉ IP, hệ điều hành..." autocomplete="off">
|
|
</div>
|
|
<div class="rb-stats">
|
|
<span>📁 Tổng: <span class="rb-stat-badge" id="rbTotalCount">{{ files_info|length
|
|
}}</span></span>
|
|
<span>🔎 Hiển thị: <span class="rb-stat-badge" id="rbFilteredCount">{{ files_info|length
|
|
}}</span></span>
|
|
<span>✅ Đã chọn: <span class="rb-stat-badge rb-stat-selected"
|
|
id="rbSelectedCount">0</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Bar -->
|
|
<div class="rb-action-bar">
|
|
<div class="rb-action-left">
|
|
<input type="checkbox" class="rb-checkbox" id="rbSelectAll" title="Chọn tất cả">
|
|
<label for="rbSelectAll" style="color: var(--text-muted); font-size: 13px; cursor: pointer;">
|
|
Chọn tất cả
|
|
</label>
|
|
</div>
|
|
<div class="rb-action-right">
|
|
<button class="rb-btn-generate" id="rbBtnGenerate" onclick="rbGenerateReport()" disabled>
|
|
📥 Xuất báo cáo tổng hợp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="rb-table-container" id="rbTableContainer">
|
|
<table class="rb-table" id="rbDataTable">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 40px;"></th>
|
|
<th>Tên máy chủ</th>
|
|
<th>Hệ điều hành</th>
|
|
<th>Địa chỉ IP</th>
|
|
<th style="text-align: center;">Tỉ lệ đạt</th>
|
|
<th style="text-align: center;">Bắt buộc</th>
|
|
<th>Đơn vị</th>
|
|
<th>Người upload</th>
|
|
<th>Ngày tạo</th>
|
|
<th style="text-align: center;">Tải về</th>
|
|
{% if show_report_builder %}<th style="text-align: center;">Xóa</th>{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rbTableBody">
|
|
{% for info in files_info %}
|
|
<tr class="rb-row" data-filename="{{ info.filename }}"
|
|
data-search="{{ info.hostname|lower }} {{ info.ip_address|lower }} {{ info.os_name|lower }} {{ info.filename|lower }} {{ info.unit_name|lower }} {{ info.uploaded_by|lower }}">
|
|
<td>
|
|
<input type="checkbox" class="rb-checkbox rb-file-cb"
|
|
data-filename="{{ info.filename }}">
|
|
</td>
|
|
<td class="rb-hostname">{{ info.hostname or info.filename }}</td>
|
|
<td><span class="rb-os">{{ info.os_name or '-' }}</span></td>
|
|
<td class="rb-ip">{{ info.ip_address or '-' }}</td>
|
|
<td style="text-align: center;">
|
|
<span
|
|
class="rb-pct-badge {% if info.compliance_percentage >= 80 %}rb-pct-green{% elif info.compliance_percentage >= 50 %}rb-pct-orange{% else %}rb-pct-red{% endif %}">
|
|
{{ info.compliance_percentage }}%
|
|
</span>
|
|
</td>
|
|
<td style="text-align: center;">
|
|
{% if info.mandatory_failed > 0 %}
|
|
<span
|
|
class="{% if info.mandatory_failed > 3 %}rb-mand-fail{% else %}rb-mand-warn{% endif %}">
|
|
{% if info.mandatory_failed > 3 %}❌{% else %}⚠️{% endif %}
|
|
{{ info.mandatory_passed }}/{{ info.mandatory_total }}
|
|
</span>
|
|
{% else %}
|
|
<span class="rb-mand-ok">✅ {{ info.mandatory_passed }}/{{ info.mandatory_total
|
|
}}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td style="white-space: nowrap;">
|
|
<span class="rb-unit-label">{{ info.unit_name or '-' }}</span>
|
|
{% if show_report_builder %}
|
|
<button class="rb-edit-unit-btn" onclick="event.stopPropagation(); rbEditUnit('{{ info.filename }}', this);"
|
|
title="Sửa đơn vị" style="background: none; border: none; cursor: pointer; font-size: 13px; padding: 2px 4px; margin-left: 4px; border-radius: 4px; transition: background 0.2s;"
|
|
onmouseover="this.style.background='#e8f0fe'" onmouseout="this.style.background='none'">✏️</button>
|
|
{% endif %}
|
|
</td>
|
|
<td style="white-space: nowrap;">{{ info.uploaded_by or '-' }}</td>
|
|
<td class="rb-date">{{ info.file_date }}</td>
|
|
<td style="text-align: center; white-space: nowrap;">
|
|
<div style="display: inline-flex; gap: 6px; align-items: center; justify-content: center;">
|
|
{% if info.filename and not info.filename.endswith('.txt') %}
|
|
<a href="{{ url_for('download_file', filename=info.filename) }}" class="rb-dl-btn-excel"
|
|
title="Tải file Excel" onclick="event.stopPropagation();">📊 Excel</a>
|
|
{% endif %}
|
|
{% if info.has_txt and info.txt_filename %}
|
|
<a href="{{ url_for('download_file', filename=info.txt_filename) }}" class="rb-dl-btn-text"
|
|
title="Tải file Text" onclick="event.stopPropagation();">📄 Text</a>
|
|
{% elif info.filename and info.filename.endswith('.txt') %}
|
|
<a href="{{ url_for('download_file', filename=info.filename) }}" class="rb-dl-btn-text"
|
|
title="Tải file Text" onclick="event.stopPropagation();">📄 Text</a>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
{% if show_report_builder %}
|
|
<td style="text-align: center;">
|
|
<button class="rb-del-btn" onclick="event.stopPropagation(); rbDeleteFile('{{ info.filename }}', this);"
|
|
title="Xóa file này" style="background: none; border: none; cursor: pointer; font-size: 16px; padding: 4px 8px; border-radius: 6px; transition: background 0.2s;"
|
|
onmouseover="this.style.background='#fee2e2'" onmouseout="this.style.background='none'">🗑️</button>
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
<div class="rb-empty-filter" id="rbEmptyFilter">
|
|
<div style="font-size: 40px; margin-bottom: 10px;">🔍</div>
|
|
<p>Không tìm thấy file nào phù hợp.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast for report builder -->
|
|
<div class="rb-toast" id="rbToast"></div>
|
|
|
|
<script>
|
|
// ROOT_PATH cho môi trường Docker reverse proxy (e.g. /audit)
|
|
const ROOT_PATH = '{{ request.scope.get("root_path", "") }}';
|
|
|
|
(function () {
|
|
const rbSelectedFiles = new Set();
|
|
|
|
document.getElementById('rbSearchInput').addEventListener('input', function () {
|
|
const query = this.value.trim().toLowerCase();
|
|
const rows = document.querySelectorAll('.rb-row');
|
|
let visibleCount = 0;
|
|
rows.forEach(row => {
|
|
const searchText = row.dataset.search || '';
|
|
if (!query || searchText.includes(query)) {
|
|
row.style.display = '';
|
|
visibleCount++;
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
document.getElementById('rbFilteredCount').textContent = visibleCount;
|
|
document.getElementById('rbEmptyFilter').style.display = visibleCount === 0 ? 'block' : 'none';
|
|
document.getElementById('rbDataTable').style.display = visibleCount === 0 ? 'none' : 'table';
|
|
rbUpdateSelectAllState();
|
|
});
|
|
|
|
document.getElementById('rbSelectAll').addEventListener('change', function () {
|
|
const checked = this.checked;
|
|
const visibleRows = document.querySelectorAll('.rb-row:not([style*="display: none"])');
|
|
visibleRows.forEach(row => {
|
|
const cb = row.querySelector('.rb-file-cb');
|
|
cb.checked = checked;
|
|
const filename = cb.dataset.filename;
|
|
if (checked) {
|
|
rbSelectedFiles.add(filename);
|
|
row.classList.add('rb-selected');
|
|
} else {
|
|
rbSelectedFiles.delete(filename);
|
|
row.classList.remove('rb-selected');
|
|
}
|
|
});
|
|
rbUpdateSelectionUI();
|
|
});
|
|
|
|
document.querySelectorAll('.rb-row').forEach(row => {
|
|
row.addEventListener('click', function (e) {
|
|
if (e.target.type === 'checkbox' || e.target.tagName === 'A' || e.target.closest('a') || e.target.closest('button')) return;
|
|
const cb = this.querySelector('.rb-file-cb');
|
|
cb.checked = !cb.checked;
|
|
rbToggleFile(cb.dataset.filename, cb.checked, this);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.rb-file-cb').forEach(cb => {
|
|
cb.addEventListener('change', function () {
|
|
rbToggleFile(this.dataset.filename, this.checked, this.closest('tr'));
|
|
});
|
|
});
|
|
|
|
function rbToggleFile(filename, selected, row) {
|
|
if (selected) {
|
|
rbSelectedFiles.add(filename);
|
|
row.classList.add('rb-selected');
|
|
} else {
|
|
rbSelectedFiles.delete(filename);
|
|
row.classList.remove('rb-selected');
|
|
}
|
|
rbUpdateSelectionUI();
|
|
}
|
|
|
|
function rbUpdateSelectionUI() {
|
|
document.getElementById('rbSelectedCount').textContent = rbSelectedFiles.size;
|
|
const btn = document.getElementById('rbBtnGenerate');
|
|
btn.disabled = rbSelectedFiles.size === 0;
|
|
if (rbSelectedFiles.size > 0) {
|
|
btn.innerHTML = `📥 Xuất báo cáo (${rbSelectedFiles.size} file)`;
|
|
} else {
|
|
btn.innerHTML = '📥 Xuất báo cáo tổng hợp';
|
|
}
|
|
rbUpdateSelectAllState();
|
|
}
|
|
|
|
function rbUpdateSelectAllState() {
|
|
const visibleCbs = document.querySelectorAll('.rb-row:not([style*="display: none"]) .rb-file-cb');
|
|
const checkedCbs = document.querySelectorAll('.rb-row:not([style*="display: none"]) .rb-file-cb:checked');
|
|
const selectAll = document.getElementById('rbSelectAll');
|
|
if (visibleCbs.length === 0) {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = false;
|
|
} else if (checkedCbs.length === visibleCbs.length) {
|
|
selectAll.checked = true;
|
|
selectAll.indeterminate = false;
|
|
} else if (checkedCbs.length > 0) {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = true;
|
|
} else {
|
|
selectAll.checked = false;
|
|
selectAll.indeterminate = false;
|
|
}
|
|
}
|
|
|
|
window.rbGenerateReport = async function () {
|
|
if (rbSelectedFiles.size === 0) return;
|
|
const btn = document.getElementById('rbBtnGenerate');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '⏳ Đang tạo báo cáo...';
|
|
|
|
try {
|
|
const response = await fetch(ROOT_PATH + '/admin/reports/generate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ files: Array.from(rbSelectedFiles) })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json();
|
|
throw new Error(err.detail || 'Failed to generate report');
|
|
}
|
|
|
|
const result = await response.json();
|
|
rbShowToast('✅ Đã tạo báo cáo: ' + result.filename, 'success');
|
|
|
|
setTimeout(function () { window.location.href = result.download_url; }, 500);
|
|
|
|
btn.innerHTML = '✅ Đã xuất (' + result.count + ' file)';
|
|
setTimeout(function () {
|
|
btn.disabled = false;
|
|
rbUpdateSelectionUI();
|
|
}, 3000);
|
|
} catch (err) {
|
|
rbShowToast('❌ Lỗi: ' + err.message, 'error');
|
|
btn.disabled = false;
|
|
rbUpdateSelectionUI();
|
|
}
|
|
};
|
|
|
|
// Hàm xóa file (chỉ admin)
|
|
window.rbDeleteFile = async function(filename, btnEl) {
|
|
if (!confirm('Bạn có chắc muốn xóa file "' + filename + '"?\n\nThao tác này không thể hoàn tác!')) return;
|
|
|
|
btnEl.disabled = true;
|
|
btnEl.textContent = '⏳';
|
|
|
|
try {
|
|
const response = await fetch(ROOT_PATH + '/api/files/' + encodeURIComponent(filename), {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
rbShowToast('✅ ' + result.message, 'success');
|
|
// Ẩn hàng khỏi bảng với animation
|
|
const row = btnEl.closest('tr');
|
|
if (row) {
|
|
row.style.transition = 'opacity 0.4s, transform 0.4s';
|
|
row.style.opacity = '0';
|
|
row.style.transform = 'translateX(30px)';
|
|
setTimeout(() => row.remove(), 400);
|
|
}
|
|
// Cập nhật counter
|
|
const totalEl = document.getElementById('rbTotalCount');
|
|
if (totalEl) totalEl.textContent = parseInt(totalEl.textContent) - 1;
|
|
const filteredEl = document.getElementById('rbFilteredCount');
|
|
if (filteredEl) filteredEl.textContent = parseInt(filteredEl.textContent) - 1;
|
|
} else {
|
|
rbShowToast('❌ ' + (result.detail || result.message || 'Lỗi xóa file'), 'error');
|
|
btnEl.disabled = false;
|
|
btnEl.textContent = '🗑️';
|
|
}
|
|
} catch (err) {
|
|
rbShowToast('❌ Lỗi kết nối: ' + err.message, 'error');
|
|
btnEl.disabled = false;
|
|
btnEl.textContent = '🗑️';
|
|
}
|
|
};
|
|
|
|
function rbShowToast(message, type) {
|
|
const toast = document.getElementById('rbToast');
|
|
toast.textContent = message;
|
|
toast.className = 'rb-toast rb-toast-' + type + ' show';
|
|
setTimeout(function () { toast.classList.remove('show'); }, 4000);
|
|
}
|
|
|
|
// Cache danh sách đơn vị
|
|
let rbUnitList = null;
|
|
async function rbLoadUnits() {
|
|
if (rbUnitList) return rbUnitList;
|
|
const resp = await fetch(ROOT_PATH + '/api/admin/units');
|
|
rbUnitList = await resp.json();
|
|
return rbUnitList;
|
|
}
|
|
|
|
window.rbEditUnit = async function(filename, btnEl) {
|
|
const units = await rbLoadUnits();
|
|
const labelEl = btnEl.parentElement.querySelector('.rb-unit-label');
|
|
const currentUnit = labelEl.textContent.trim();
|
|
|
|
// Tạo select dropdown inline
|
|
let select = document.createElement('select');
|
|
select.style.cssText = 'font-size:13px;padding:2px 6px;border-radius:4px;border:1px solid #ccc;max-width:180px';
|
|
select.innerHTML = '<option value="">-- Chọn --</option>';
|
|
units.forEach(u => {
|
|
const opt = document.createElement('option');
|
|
opt.value = u.unit_id;
|
|
opt.textContent = u.unit_name;
|
|
opt.dataset.unitName = u.unit_name;
|
|
if (u.unit_name === currentUnit) opt.selected = true;
|
|
select.appendChild(opt);
|
|
});
|
|
|
|
// Thay label bằng select
|
|
labelEl.style.display = 'none';
|
|
btnEl.style.display = 'none';
|
|
labelEl.parentElement.insertBefore(select, labelEl);
|
|
|
|
// Nút lưu / hủy
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.textContent = '✓';
|
|
saveBtn.style.cssText = 'background:#4caf50;color:white;border:none;cursor:pointer;font-size:13px;padding:2px 6px;margin-left:3px;border-radius:4px';
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = '✕';
|
|
cancelBtn.style.cssText = 'background:#f44336;color:white;border:none;cursor:pointer;font-size:13px;padding:2px 6px;margin-left:3px;border-radius:4px';
|
|
|
|
select.parentElement.appendChild(saveBtn);
|
|
select.parentElement.appendChild(cancelBtn);
|
|
|
|
const cleanup = () => {
|
|
select.remove();
|
|
saveBtn.remove();
|
|
cancelBtn.remove();
|
|
labelEl.style.display = '';
|
|
btnEl.style.display = '';
|
|
};
|
|
|
|
cancelBtn.onclick = cleanup;
|
|
|
|
saveBtn.onclick = async () => {
|
|
const unitId = select.value;
|
|
const unitName = select.options[select.selectedIndex].dataset.unitName;
|
|
if (!unitId) { cleanup(); return; }
|
|
|
|
saveBtn.disabled = true;
|
|
saveBtn.textContent = '...';
|
|
try {
|
|
const resp = await fetch(ROOT_PATH + '/api/admin/files/' + encodeURIComponent(filename) + '/unit', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ unit_id: unitId, unit_name: unitName })
|
|
});
|
|
const result = await resp.json();
|
|
if (resp.ok) {
|
|
labelEl.textContent = unitName;
|
|
rbShowToast('✅ ' + result.message, 'success');
|
|
} else {
|
|
rbShowToast('❌ ' + (result.detail || 'Lỗi'), 'error');
|
|
}
|
|
} catch (err) {
|
|
rbShowToast('❌ Lỗi kết nối: ' + err.message, 'error');
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
};
|
|
};
|
|
})();
|
|
</script>
|
|
|
|
{% elif output_files and output_files|length > 0 and not session_mode and not show_report_builder %}
|
|
<div class="success-icon">✅</div>
|
|
<h1>Xử lý hoàn tất!</h1>
|
|
<p class="subtitle">Báo cáo của bạn đã sẵn sàng để tải xuống</p>
|
|
|
|
{% if system_info_list and system_info_list|length > 0 %}
|
|
<!-- ===== BẢNG TỔNG HỢP KẾT QUẢ ===== -->
|
|
<div style="margin: 25px 0;">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
<h2 style="color: var(--primary-color); margin: 0; font-size: 20px; display: flex; align-items: center; gap: 10px;">
|
|
📋 Bảng tổng hợp kết quả
|
|
</h2>
|
|
<button id="btnExportCSV" onclick="exportSummaryCSV()" style="
|
|
padding: 8px 18px;
|
|
background: linear-gradient(135deg, #43a047, #2e7d32);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 2px 8px rgba(46, 125, 50, 0.3);
|
|
">
|
|
📥 Xuất CSV
|
|
</button>
|
|
</div>
|
|
|
|
<div style="overflow-x: auto; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08);">
|
|
<table id="summaryTable" style="
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
background: var(--card-bg);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
font-size: 14px;
|
|
">
|
|
<thead>
|
|
<tr style="background: linear-gradient(135deg, #1565c0 0%, #1976d2 100%);">
|
|
<th
|
|
style="padding: 14px 16px; color: white; font-weight: 600; text-align: center; font-size: 13px; letter-spacing: 0.5px; white-space: nowrap;">
|
|
STT</th>
|
|
<th
|
|
style="padding: 14px 16px; color: white; font-weight: 600; text-align: left; font-size: 13px; letter-spacing: 0.5px; white-space: nowrap;">
|
|
🖥️ Tên máy chủ</th>
|
|
<th
|
|
style="padding: 14px 16px; color: white; font-weight: 600; text-align: left; font-size: 13px; letter-spacing: 0.5px; white-space: nowrap;">
|
|
🌐 Địa chỉ IP</th>
|
|
<th
|
|
style="padding: 14px 16px; color: white; font-weight: 600; text-align: center; font-size: 13px; letter-spacing: 0.5px; white-space: nowrap;">
|
|
📊 Tỉ lệ đạt</th>
|
|
<th
|
|
style="padding: 14px 16px; color: white; font-weight: 600; text-align: center; font-size: 13px; letter-spacing: 0.5px; white-space: nowrap;">
|
|
🔒 Bắt buộc đạt</th>
|
|
<th
|
|
style="padding: 14px 16px; color: white; font-weight: 600; text-align: left; font-size: 13px; letter-spacing: 0.5px; min-width: 200px;">
|
|
📝 Ghi chú</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for system_info in system_info_list %}
|
|
<tr style="
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: all 0.2s ease;
|
|
" onmouseover="this.style.background='#f5f9ff'"
|
|
onmouseout="this.style.background='{% if loop.index is odd %}#fafbfc{% else %}white{% endif %}'">
|
|
<td
|
|
style="padding: 12px 16px; text-align: center; color: var(--text-muted); font-weight: 500; background: {% if loop.index is odd %}#fafbfc{% else %}white{% endif %};">
|
|
{{ loop.index }}
|
|
</td>
|
|
<td
|
|
style="padding: 12px 16px; font-weight: 600; color: var(--primary-color); background: {% if loop.index is odd %}#fafbfc{% else %}white{% endif %};">
|
|
{{ system_info.hostname or 'N/A' }}
|
|
{% if system_info.os_name %}
|
|
<div style="font-size: 11px; color: var(--text-muted); font-weight: 400; margin-top: 2px;">{{
|
|
system_info.os_name }}</div>
|
|
{% endif %}
|
|
</td>
|
|
<td
|
|
style="padding: 12px 16px; color: var(--text-main); font-family: 'Consolas', 'Courier New', monospace; font-size: 13px; background: {% if loop.index is odd %}#fafbfc{% else %}white{% endif %};">
|
|
{{ system_info.ip_address or 'N/A' }}
|
|
</td>
|
|
<td
|
|
style="padding: 12px 16px; text-align: center; background: {% if loop.index is odd %}#fafbfc{% else %}white{% endif %};">
|
|
<span style="
|
|
display: inline-block;
|
|
padding: 5px 14px;
|
|
border-radius: 20px;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
{% if system_info.compliance_percentage >= 80 %}
|
|
background-color: var(--success-bg); color: var(--success-color);
|
|
{% elif system_info.compliance_percentage >= 50 %}
|
|
background-color: var(--warning-bg); color: var(--warning-color);
|
|
{% else %}
|
|
background-color: var(--danger-bg); color: var(--danger-color);
|
|
{% endif %}
|
|
">
|
|
{{ system_info.compliance_percentage }}%
|
|
</span>
|
|
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
|
|
{{ system_info.passed_count }}/{{ system_info.total_checks }} tiêu chí
|
|
</div>
|
|
</td>
|
|
<td
|
|
style="padding: 12px 16px; text-align: center; background: {% if loop.index is odd %}#fafbfc{% else %}white{% endif %};">
|
|
<span style="
|
|
display: inline-block;
|
|
padding: 5px 14px;
|
|
border-radius: 20px;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
{% if system_info.mandatory_failed == 0 and system_info.mandatory_total > 0 %}
|
|
background-color: var(--success-bg); color: var(--success-color);
|
|
{% elif system_info.mandatory_total > 0 %}
|
|
background-color: var(--danger-bg); color: var(--danger-color);
|
|
{% else %}
|
|
background: var(--bg-main); color: var(--text-muted);
|
|
{% endif %}
|
|
">
|
|
{{ system_info.mandatory_passed }}/{{ system_info.mandatory_total }}
|
|
</span>
|
|
{% if system_info.mandatory_total > 0 %}
|
|
<div style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">
|
|
{{ system_info.mandatory_percentage }}%
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td
|
|
style="padding: 12px 16px; font-size: 13px; background: {% if loop.index is odd %}#fafbfc{% else %}white{% endif %};">
|
|
{% if system_info.mandatory_failed > 0 and system_info.compliance_percentage < 80 %}
|
|
<div style="display: flex; flex-direction: column; gap: 6px;">
|
|
<div style="
|
|
display: flex; align-items: flex-start; gap: 6px;
|
|
padding: 8px 12px; background-color: var(--danger-bg);
|
|
border-left: 3px solid #c62828; border-radius: 6px;
|
|
color: var(--danger-color); font-weight: 500;
|
|
">
|
|
❌ Còn <strong style="margin: 0 3px;">{{ system_info.mandatory_failed }}</strong>
|
|
tiêu chí bắt buộc chưa đạt
|
|
</div>
|
|
<div style="
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 8px 12px; background-color: var(--warning-bg);
|
|
border-left: 3px solid #ffa000; border-radius: 6px;
|
|
color: var(--warning-color); font-weight: 500;
|
|
">
|
|
⚠️ Tỉ lệ đạt chỉ {{ system_info.compliance_percentage }}% (yêu cầu ≥80%)
|
|
</div>
|
|
</div>
|
|
{% elif system_info.mandatory_failed > 0 %}
|
|
<div style="
|
|
display: flex; align-items: flex-start; gap: 6px;
|
|
padding: 8px 12px; background-color: var(--warning-bg);
|
|
border-left: 3px solid #ffa000; border-radius: 6px;
|
|
color: var(--warning-color); font-weight: 500;
|
|
">
|
|
⚠️ Còn <strong style="color: var(--danger-color); margin: 0 3px;">{{ system_info.mandatory_failed }}</strong> tiêu
|
|
chí bắt buộc chưa đạt
|
|
</div>
|
|
{% elif system_info.compliance_percentage < 80 %} <div style="
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 8px 12px; background-color: var(--warning-bg);
|
|
border-left: 3px solid #ffa000; border-radius: 6px;
|
|
color: var(--warning-color); font-weight: 500;
|
|
">
|
|
⚠️ Tỉ lệ đạt chỉ {{ system_info.compliance_percentage }}% (yêu cầu ≥80%)
|
|
</div>
|
|
{% elif system_info.mandatory_total > 0 %}
|
|
<div style="
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 8px 12px; background-color: var(--success-bg);
|
|
border-left: 3px solid #4caf50; border-radius: 6px;
|
|
color: var(--success-color); font-weight: 500;
|
|
">
|
|
✅ Đạt yêu cầu
|
|
</div>
|
|
{% else %}
|
|
<span style="color: #bbb; font-style: italic;">Không có dữ liệu</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
<!-- Summary Footer Row -->
|
|
{% if system_info_list|length > 1 %}
|
|
<tfoot>
|
|
<tr style="background: linear-gradient(135deg, #f5f5f5, #eeeeee); font-weight: 600;">
|
|
<td colspan="3"
|
|
style="padding: 14px 16px; text-align: right; color: var(--text-main); font-size: 14px; border-top: 2px solid #e0e0e0;">
|
|
📈 Tổng hợp ({{ system_info_list|length }} máy chủ):
|
|
</td>
|
|
<td style="padding: 14px 16px; text-align: center; border-top: 2px solid #e0e0e0;">
|
|
{% set ns = namespace(total_passed=0, total_checks=0) %}
|
|
{% for si in system_info_list %}
|
|
{% set ns.total_passed = ns.total_passed + si.passed_count %}
|
|
{% set ns.total_checks = ns.total_checks + si.total_checks %}
|
|
{% endfor %}
|
|
{% if ns.total_checks > 0 %}
|
|
{% set avg_pct = (ns.total_passed / ns.total_checks * 100)|round(1) %}
|
|
{% else %}
|
|
{% set avg_pct = 0 %}
|
|
{% endif %}
|
|
<span style="
|
|
display: inline-block;
|
|
padding: 5px 14px;
|
|
border-radius: 20px;
|
|
font-weight: 700;
|
|
font-size: 14px;
|
|
{% if avg_pct >= 80 %}
|
|
background-color: var(--success-bg); color: var(--success-color);
|
|
{% elif avg_pct >= 50 %}
|
|
background-color: var(--warning-bg); color: var(--warning-color);
|
|
{% else %}
|
|
background-color: var(--danger-bg); color: var(--danger-color);
|
|
{% endif %}
|
|
">
|
|
{{ avg_pct }}%
|
|
</span>
|
|
</td>
|
|
<td style="padding: 14px 16px; text-align: center; border-top: 2px solid #e0e0e0;">
|
|
{% set ns2 = namespace(total_mp=0, total_mt=0) %}
|
|
{% for si in system_info_list %}
|
|
{% set ns2.total_mp = ns2.total_mp + si.mandatory_passed %}
|
|
{% set ns2.total_mt = ns2.total_mt + si.mandatory_total %}
|
|
{% endfor %}
|
|
<span style="font-size: 14px; color: var(--text-main);">
|
|
{{ ns2.total_mp }}/{{ ns2.total_mt }}
|
|
</span>
|
|
</td>
|
|
<td style="padding: 14px 16px; border-top: 2px solid #e0e0e0;">
|
|
{% set ns3 = namespace(total_mf=0) %}
|
|
{% for si in system_info_list %}
|
|
{% set ns3.total_mf = ns3.total_mf + si.mandatory_failed %}
|
|
{% endfor %}
|
|
{% if ns3.total_mf > 0 %}
|
|
<span style="color: var(--danger-color); font-weight: 600;">
|
|
⚠️ Tổng {{ ns3.total_mf }} tiêu chí bắt buộc chưa đạt
|
|
</span>
|
|
{% else %}
|
|
<span style="color: var(--success-color); font-weight: 600;">
|
|
✅ Tất cả đạt
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
</tfoot>
|
|
{% endif %}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== THÔNG TIN CHI TIẾT TỪNG MÁY CHỦ (Collapsible) ===== -->
|
|
<details id="detailsSection" style="margin: 25px 0;">
|
|
<summary style="
|
|
color: var(--primary-color);
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 20px;
|
|
background: linear-gradient(135deg, #e3f2fd 0%, #f5f5f5 100%);
|
|
border-radius: 12px;
|
|
border: 1px solid #bbdefb;
|
|
transition: all 0.3s ease;
|
|
list-style: none;
|
|
user-select: none;
|
|
">
|
|
<span style="
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
font-size: 12px;
|
|
transition: transform 0.3s ease;
|
|
" class="collapse-arrow">▶</span>
|
|
🖥️ Thông tin chi tiết hệ thống
|
|
<span style="
|
|
margin-left: auto;
|
|
background: #1976d2;
|
|
color: white;
|
|
padding: 3px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
">{{ system_info_list|length }} máy chủ</span>
|
|
</summary>
|
|
<div style="margin-top: 20px;">
|
|
{% for system_info in system_info_list %}
|
|
<div
|
|
style="background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); border: 1px solid var(--border-color); border-radius: 15px; padding: 25px; margin-bottom: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.05);">
|
|
<!-- Header with hostname and OS -->
|
|
<div
|
|
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid var(--sub-tab-border);">
|
|
<div>
|
|
<h3 style="color: var(--primary-color); margin: 0; font-size: 22px;">{{ system_info.hostname or 'Unknown
|
|
Host' }}</h3>
|
|
<p style="color: var(--text-muted); margin: 5px 0 0 0; font-size: 14px;">{{ system_info.os_name }}</p>
|
|
</div>
|
|
<div
|
|
style="text-align: right; display: flex; flex-direction: column; gap: 5px; align-items: flex-end;">
|
|
<span
|
|
style="background-color: var(--info-bg); color: var(--primary-color); padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">
|
|
{{ system_info.audit_time }}
|
|
</span>
|
|
{% if system_info.script_version %}
|
|
<span
|
|
style="background-color: var(--success-bg); color: var(--success-color); padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 600;">
|
|
🏷️ Script v{{ system_info.script_version }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Grid -->
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px;">
|
|
<!-- Basic Info Card -->
|
|
<div style="background: #fff; border-radius: 10px; padding: 20px; border: 1px solid #e8e8e8;">
|
|
<h4
|
|
style="color: var(--primary-color); margin: 0 0 15px 0; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
|
|
📋 Thông tin cơ bản
|
|
</h4>
|
|
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
{% if system_info.ip_address %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">IP Address:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.ip_address }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.kernel_version %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Kernel:</span>
|
|
<strong style="color: var(--text-main); font-size: 12px;">{{ system_info.kernel_version }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.architecture %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Architecture:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.architecture }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.uptime %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Uptime:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.uptime }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.timezone %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Timezone:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.timezone }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hardware Info Card -->
|
|
<div style="background: #fff; border-radius: 10px; padding: 20px; border: 1px solid #e8e8e8;">
|
|
<h4
|
|
style="color: var(--success-color); margin: 0 0 15px 0; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
|
|
⚙️ Phần cứng
|
|
</h4>
|
|
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
{% if system_info.cpu_model %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">CPU:</span>
|
|
<strong style="color: var(--text-main); font-size: 12px; text-align: right; max-width: 180px;">{{
|
|
system_info.cpu_model }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.cpu_cores %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">CPU Cores:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.cpu_cores }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.total_memory %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Total Memory:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.total_memory }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.used_memory %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Used Memory:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.used_memory }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.free_memory %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Free Memory:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.free_memory }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Info Card -->
|
|
<div style="background: #fff; border-radius: 10px; padding: 20px; border: 1px solid #e8e8e8;">
|
|
<h4
|
|
style="color: var(--warning-color); margin: 0 0 15px 0; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
|
|
🌐 Mạng
|
|
</h4>
|
|
<div style="display: flex; flex-direction: column; gap: 10px;">
|
|
{% if system_info.primary_interface %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Interface:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.primary_interface }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.mac_address %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">MAC:</span>
|
|
<strong style="color: var(--text-main); font-size: 12px;">{{ system_info.mac_address }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.default_gateway %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">Gateway:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.default_gateway }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
{% if system_info.dns_servers %}
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-muted);">DNS:</span>
|
|
<strong style="color: var(--text-main);">{{ system_info.dns_servers }}</strong>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compliance Statistics Card -->
|
|
<div style="background: #fff; border-radius: 10px; padding: 20px; border: 1px solid #e8e8e8;">
|
|
<h4
|
|
style="color: #e91e63; margin: 0 0 15px 0; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
|
|
📊 Tỉ lệ tuân thủ
|
|
</h4>
|
|
<div style="display: flex; flex-direction: column; align-items: center; gap: 15px;">
|
|
<!-- Circular Progress Indicator -->
|
|
<div style="position: relative; width: 120px; height: 120px;">
|
|
<svg width="120" height="120" viewBox="0 0 120 120" style="transform: rotate(-90deg);">
|
|
<circle cx="60" cy="60" r="50" stroke="#e0e0e0" stroke-width="10" fill="none">
|
|
</circle>
|
|
<circle cx="60" cy="60" r="50"
|
|
stroke="{% if system_info.compliance_percentage >= 80 %}#4caf50{% elif system_info.compliance_percentage >= 50 %}#ff9800{% else %}#f44336{% endif %}"
|
|
stroke-width="10" fill="none" stroke-linecap="round"
|
|
stroke-dasharray="{{ (system_info.compliance_percentage * 314.16 / 100)|round(2) }} 314.16">
|
|
</circle>
|
|
</svg>
|
|
<div
|
|
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
|
|
<span
|
|
style="font-size: 28px; font-weight: bold; color: {% if system_info.compliance_percentage >= 80 %}#4caf50{% elif system_info.compliance_percentage >= 50 %}#ff9800{% else %}#f44336{% endif %};">
|
|
{{ system_info.compliance_percentage }}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mandatory / Non-Mandatory Stats -->
|
|
<div style="display: flex; gap: 15px; justify-content: center; width: 100%;">
|
|
<div
|
|
style="text-align: center; padding: 12px 15px; background-color: var(--info-bg); border-radius: 8px; flex: 1; border-left: 3px solid #1565c0;">
|
|
<div
|
|
style="font-size: 11px; color: var(--primary-color); font-weight: 600; text-transform: uppercase; margin-bottom: 6px;">
|
|
🔒 Bắt buộc đạt
|
|
</div>
|
|
<div
|
|
style="font-size: 22px; font-weight: bold; color: {% if system_info.mandatory_passed == system_info.mandatory_total and system_info.mandatory_total > 0 %}#4caf50{% elif system_info.mandatory_total > 0 %}#ff9800{% else %}#666{% endif %};">
|
|
{{ system_info.mandatory_passed }}/{{ system_info.mandatory_total }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
style="text-align: center; padding: 12px 15px; background: #f3e5f5; border-radius: 8px; flex: 1; border-left: 3px solid #7b1fa2;">
|
|
<div
|
|
style="font-size: 11px; color: #7b1fa2; font-weight: 600; text-transform: uppercase; margin-bottom: 6px;">
|
|
📋 Tuỳ chọn đạt
|
|
</div>
|
|
<div
|
|
style="font-size: 22px; font-weight: bold; color: {% if system_info.optional_passed == system_info.optional_total and system_info.optional_total > 0 %}#4caf50{% elif system_info.optional_total > 0 %}#ff9800{% else %}#666{% endif %};">
|
|
{{ system_info.optional_passed }}/{{ system_info.optional_total }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total checks -->
|
|
<div style="text-align: center; color: var(--text-muted); font-size: 12px; margin-top: 5px;">
|
|
Tổng số: <strong>{{ system_info.total_checks }}</strong> tiêu chí
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</details>
|
|
{% endif %}
|
|
|
|
<!-- Download All button -->
|
|
{% if output_files|length > 1 %}
|
|
<div style="margin-bottom: 15px; display: flex; justify-content: flex-end;">
|
|
<button id="btnDownloadAll" onclick="downloadAllFiles()" style="
|
|
padding: 12px 24px;
|
|
background: linear-gradient(135deg, #1565c0, #1976d2);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(25, 118, 210, 0.3);
|
|
">
|
|
📦 Tải tất cả ({{ output_files|length }} file)
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<ul class="file-list">
|
|
{% for file in output_files %}
|
|
<li class="file-item">
|
|
<div style="display: flex; align-items: center; flex: 1;">
|
|
<span class="file-icon">📊</span>
|
|
<span class="file-name">{{ file }}</span>
|
|
</div>
|
|
<a href="{{ url_for('download_file', filename=file) }}" class="btn-download">⬇️ Download</a>
|
|
</li>
|
|
{% endfor %}
|
|
|
|
{% if is_admin and decrypted_files %}
|
|
<!-- Decrypted TXT files - Only for Admin -->
|
|
{% for file in decrypted_files %}
|
|
<li class="file-item" style="background-color: var(--warning-bg); border-left: 3px solid #ff9800;">
|
|
<div style="display: flex; align-items: center; flex: 1;">
|
|
<span class="file-icon">📃</span>
|
|
<span class="file-name">{{ file }}</span>
|
|
<span
|
|
style="margin-left: 10px; background: #ff9800; color: white; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600;">RAW</span>
|
|
</div>
|
|
<a href="{{ url_for('download_file', filename=file) }}" class="btn-download" style="background: #ff9800;">⬇️
|
|
Download</a>
|
|
</li>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</ul>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">📁</div>
|
|
<h1>Không có file kết quả</h1>
|
|
<p class="subtitle">Không tìm thấy file đã xử lý trong thư mục output</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if session_mode %}
|
|
<div class="action-buttons">
|
|
<a href="{{ url_for('index') }}" class="btn-secondary">🔙 Xử lý thêm File</a>
|
|
<a href="{{ url_for('list_outputs') }}" class="btn-primary">🔒 Xem tất cả File</a>
|
|
</div>
|
|
{% else %}
|
|
<a href="{{ url_for('index') }}" class="btn-back">🔙 Xử lý thêm File</a>
|
|
{% endif %}
|
|
|
|
<div class="footer"
|
|
style="text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: var(--text-muted); font-size: 13px;">
|
|
<p>Công cụ Kiểm tra Hardening v2.3 - VNPT-MEDIA</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Download notification handler
|
|
const downloadLinks = document.querySelectorAll('.btn-download');
|
|
const notification = document.getElementById('downloadNotification');
|
|
const downloadText = document.getElementById('downloadText');
|
|
|
|
downloadLinks.forEach(link => {
|
|
link.addEventListener('click', function (e) {
|
|
const filename = this.getAttribute('href').split('/').pop();
|
|
|
|
// Show notification
|
|
downloadText.textContent = `Đang tải ${filename}...`;
|
|
notification.classList.add('show');
|
|
|
|
// Hide notification after 3 seconds
|
|
setTimeout(() => {
|
|
downloadText.textContent = '✓ Tải xuống hoàn tất!';
|
|
notification.querySelector('.download-spinner').style.display = 'none';
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
notification.querySelector('.download-spinner').style.display = 'block';
|
|
}, 2000);
|
|
}, 1500);
|
|
});
|
|
});
|
|
|
|
// Export summary table to CSV
|
|
function exportSummaryCSV() {
|
|
const table = document.getElementById('summaryTable');
|
|
if (!table) return;
|
|
|
|
const rows = table.querySelectorAll('thead tr, tbody tr');
|
|
let csvContent = '\uFEFF'; // BOM for UTF-8 Excel compatibility
|
|
|
|
rows.forEach((row, index) => {
|
|
const cells = row.querySelectorAll('th, td');
|
|
const rowData = [];
|
|
|
|
cells.forEach(cell => {
|
|
// Get text content and clean it
|
|
let text = cell.textContent.trim()
|
|
.replace(/\s+/g, ' ') // collapse whitespace
|
|
.replace(/"/g, '""'); // escape quotes
|
|
rowData.push('"' + text + '"');
|
|
});
|
|
|
|
csvContent += rowData.join(',') + '\n';
|
|
});
|
|
|
|
// Add footer row if exists
|
|
const footerRows = table.querySelectorAll('tfoot tr');
|
|
footerRows.forEach(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
const rowData = [];
|
|
cells.forEach(cell => {
|
|
let text = cell.textContent.trim()
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/"/g, '""');
|
|
rowData.push('"' + text + '"');
|
|
});
|
|
csvContent += rowData.join(',') + '\n';
|
|
});
|
|
|
|
// Create and trigger download
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const now = new Date();
|
|
const timestamp = now.getFullYear() +
|
|
String(now.getMonth() + 1).padStart(2, '0') +
|
|
String(now.getDate()).padStart(2, '0') + '_' +
|
|
String(now.getHours()).padStart(2, '0') +
|
|
String(now.getMinutes()).padStart(2, '0');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = `Tong_hop_ket_qua_audit_${timestamp}.csv`;
|
|
link.style.display = 'none';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(link.href);
|
|
|
|
// Show notification
|
|
const notification = document.getElementById('downloadNotification');
|
|
const downloadText = document.getElementById('downloadText');
|
|
if (notification && downloadText) {
|
|
downloadText.textContent = '✓ Đã xuất file CSV!';
|
|
notification.querySelector('.download-spinner').style.display = 'none';
|
|
notification.classList.add('show');
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
notification.querySelector('.download-spinner').style.display = 'block';
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
// Collapsible details arrow rotation
|
|
const detailsSection = document.getElementById('detailsSection');
|
|
if (detailsSection) {
|
|
detailsSection.addEventListener('toggle', function () {
|
|
const arrow = this.querySelector('.collapse-arrow');
|
|
if (arrow) {
|
|
arrow.style.transform = this.open ? 'rotate(90deg)' : 'rotate(0deg)';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Download all files sequentially
|
|
function downloadAllFiles() {
|
|
const fileLinks = document.querySelectorAll('.file-list .btn-download');
|
|
if (fileLinks.length === 0) return;
|
|
|
|
const btn = document.getElementById('btnDownloadAll');
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.style.opacity = '0.7';
|
|
btn.innerHTML = '⏳ Đang tải...';
|
|
}
|
|
|
|
let i = 0;
|
|
function downloadNext() {
|
|
if (i >= fileLinks.length) {
|
|
// All done
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.style.opacity = '1';
|
|
btn.innerHTML = '✅ Đã tải xong!';
|
|
setTimeout(() => {
|
|
btn.innerHTML = `📦 Tải tất cả (${fileLinks.length} file)`;
|
|
}, 3000);
|
|
}
|
|
|
|
// Show notification
|
|
const notification = document.getElementById('downloadNotification');
|
|
const downloadText = document.getElementById('downloadText');
|
|
if (notification && downloadText) {
|
|
downloadText.textContent = `✓ Đã tải ${fileLinks.length} file!`;
|
|
notification.querySelector('.download-spinner').style.display = 'none';
|
|
notification.classList.add('show');
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
notification.querySelector('.download-spinner').style.display = 'block';
|
|
}, 3000);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Create a hidden iframe to trigger download without navigating
|
|
const iframe = document.createElement('iframe');
|
|
iframe.style.display = 'none';
|
|
iframe.src = fileLinks[i].href;
|
|
document.body.appendChild(iframe);
|
|
|
|
// Clean up iframe after download starts
|
|
setTimeout(() => {
|
|
document.body.removeChild(iframe);
|
|
}, 5000);
|
|
|
|
i++;
|
|
// Delay between downloads to prevent browser blocking
|
|
setTimeout(downloadNext, 800);
|
|
}
|
|
|
|
downloadNext();
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |