Files
2026-08-26 14:11:37 +07:00

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>