Files
remote-sensing/index.html
T
2025-12-22 15:43:23 +07:00

1002 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Land Classification System - Complete Platform</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.main-container {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
margin-bottom: 20px;
text-align: center;
}
.header h1 {
color: #667eea;
font-size: 2.8em;
margin-bottom: 10px;
font-weight: 700;
}
.header p {
color: #666;
font-size: 1.2em;
}
/* Navigation Tabs */
.nav-tabs {
background: white;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
padding: 15px;
margin-bottom: 20px;
display: flex;
gap: 10px;
overflow-x: auto;
}
.nav-tab {
flex: 1;
min-width: 150px;
padding: 15px 25px;
background: #f5f5f5;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: all 0.3s;
color: #666;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.nav-tab:hover {
background: #e0e0e0;
transform: translateY(-2px);
}
.nav-tab.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
/* Tab Content */
.tab-content {
display: none;
animation: fadeIn 0.3s;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Content Container */
.content-wrapper {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
padding: 30px;
min-height: 600px;
}
/* Common Styles */
.section {
margin-bottom: 30px;
}
.section h2 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.8em;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.section h3 {
color: #333;
margin-bottom: 15px;
font-size: 1.3em;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.6);
}
.btn-success {
background: #4caf50;
color: white;
}
.btn-success:hover {
background: #45a049;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
/* Grid layouts */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
@media (max-width: 768px) {
.grid-2 {
grid-template-columns: 1fr;
}
}
/* Cards */
.card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border: 2px solid #e0e0e0;
transition: all 0.3s;
}
.card:hover {
border-color: #667eea;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
}
/* Stats cards */
.stat-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card .icon {
font-size: 3em;
margin-bottom: 15px;
}
.stat-card .value {
font-size: 2.5em;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-card .label {
color: #666;
font-size: 1.1em;
}
/* Alert boxes */
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.alert-info {
background: #e3f2fd;
border-left: 4px solid #2196f3;
color: #1565c0;
}
.alert-success {
background: #e8f5e9;
border-left: 4px solid #4caf50;
color: #2e7d32;
}
.alert-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
color: #856404;
}
.alert-danger {
background: #ffebee;
border-left: 4px solid #f44336;
color: #c62828;
}
/* Progress bar */
.progress {
width: 100%;
height: 30px;
background: #e0e0e0;
border-radius: 15px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
/* Map styles */
#trainMap, #predictMap {
height: 500px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
/* Loading spinner */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
}
.status-badge.running {
background: #d4edda;
color: #155724;
}
.status-badge.completed {
background: #cce5ff;
color: #004085;
}
.status-badge.error {
background: #f8d7da;
color: #721c24;
}
/* Table */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
th {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
tr:hover {
background: #f9f9f9;
}
/* Footer */
.footer {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
margin-top: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="main-container">
<!-- Header -->
<div class="header">
<h1>🛰️ Land Classification System</h1>
<p>Hệ thống phân loại đất từ xa sử dụng Sentinel-2 & Sentinel-1</p>
</div>
<!-- Navigation Tabs -->
<div style="background: white; padding: 15px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; border-bottom: 2px solid #e0e0e0;">
<a href="/" style="padding: 10px 20px; background: #667eea; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🏠 Trang Chủ (Active)</a>
<a href="/training" style="padding: 10px 20px; background: #f093fb; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🎓 Training</a>
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</a>
<a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing</a>
<a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis</a>
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
</div>
<!-- Tab Content: Home -->
<div id="home" class="tab-content active">
<div class="content-wrapper">
<div class="section">
<h2>🎯 Chào mừng đến với Land Classification System</h2>
<p style="font-size: 1.2em; color: #666; margin-bottom: 30px;">
Nền tảng phân loại đất tự động sử dụng dữ liệu vệ tinh Sentinel và Machine Learning
</p>
</div>
<div class="grid-3">
<div class="stat-card">
<div class="icon">🎓</div>
<div class="value" id="homeModelsCount">-</div>
<div class="label">Models Trained</div>
</div>
<div class="stat-card">
<div class="icon">🗺️</div>
<div class="value" id="homePredictionsCount">-</div>
<div class="label">Predictions Created</div>
</div>
<div class="stat-card">
<div class="icon">📄</div>
<div class="value" id="homeReportsCount">-</div>
<div class="label">Reports Generated</div>
</div>
</div>
<div class="section" style="margin-top: 40px;">
<h3>🚀 Bắt đầu nhanh</h3>
<div class="grid-2">
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">1️⃣ Training Model</h4>
<p style="color: #666; margin-bottom: 15px;">
Train model mới với dữ liệu Sentinel-2/1 và shapefile training data
</p>
<button class="btn btn-primary" onclick="switchTab('train')">
🎓 Bắt đầu Training
</button>
</div>
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">2️⃣ Prediction</h4>
<p style="color: #666; margin-bottom: 15px;">
Sử dụng model đã train để phân loại khu vực mới
</p>
<button class="btn btn-success" onclick="switchTab('predict')">
🗺️ Bắt đầu Prediction
</button>
</div>
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">3️⃣ Dashboard</h4>
<p style="color: #666; margin-bottom: 15px;">
Xem thống kê, biểu đồ accuracy trends và so sánh models
</p>
<button class="btn btn-secondary" onclick="switchTab('dashboard')">
📊 Mở Dashboard
</button>
</div>
<div class="card">
<h4 style="color: #667eea; margin-bottom: 10px;">4️⃣ Batch Processing</h4>
<p style="color: #666; margin-bottom: 15px;">
Predict nhiều khu vực cùng lúc với CSV file
</p>
<button class="btn btn-secondary" onclick="switchTab('batch')">
🔄 Batch Processing
</button>
</div>
</div>
</div>
<div class="section" style="margin-top: 40px;">
<h3>📚 Tài liệu & Hướng dẫn</h3>
<div class="alert alert-info">
<span style="font-size: 1.5em;"></span>
<div>
<strong>API Documentation:</strong>
<a href="/docs" target="_blank" style="color: #1565c0; text-decoration: none; font-weight: 600;">
/docs
</a>
<br>
<strong>Features Guide:</strong> Xem file NEW_FEATURES.md để biết chi tiết
</div>
</div>
</div>
</div>
</div>
<!-- Tab Content: Training -->
<div id="train" class="tab-content">
<div class="content-wrapper">
<iframe src="/training" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
</div>
</div>
<!-- Tab Content: Prediction -->
<div id="predict" class="tab-content">
<div class="content-wrapper">
<iframe src="/prediction" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
</div>
</div>
<!-- Tab Content: Dashboard -->
<div id="dashboard" class="tab-content">
<div class="content-wrapper">
<iframe src="/dashboard" style="width: 100%; height: 800px; border: none; border-radius: 10px;"></iframe>
</div>
</div>
<!-- Tab Content: Models -->
<div id="models" class="tab-content">
<div class="content-wrapper">
<div class="section">
<h2>🤖 Model Management</h2>
<p style="color: #666; margin-bottom: 20px;">Quản lý các models đã train</p>
</div>
<div id="modelsLoading" class="loading">
<div class="spinner"></div>
<p>Đang tải danh sách models...</p>
</div>
<div id="modelsList" style="display: none;">
<table>
<thead>
<tr>
<th>Tên File</th>
<th>Model Type</th>
<th>Accuracy</th>
<th>Ngày Tạo</th>
<th>Kích Thước</th>
<th>Thao Tác</th>
</tr>
</thead>
<tbody id="modelsTableBody"></tbody>
</table>
</div>
</div>
</div>
<!-- Tab Content: Reports -->
<div id="reports" class="tab-content">
<div class="content-wrapper">
<div class="section">
<h2>📄 Reports Management</h2>
<p style="color: #666; margin-bottom: 20px;">Quản lý các báo cáo đã tạo</p>
</div>
<div id="reportsLoading" class="loading">
<div class="spinner"></div>
<p>Đang tải danh sách reports...</p>
</div>
<div id="reportsList" style="display: none;">
<table>
<thead>
<tr>
<th>Tên File</th>
<th>Loại</th>
<th>Ngày Tạo</th>
<th>Kích Thước</th>
<th>Thao Tác</th>
</tr>
</thead>
<tbody id="reportsTableBody"></tbody>
</table>
</div>
</div>
</div>
<!-- Tab Content: Batch Processing -->
<div id="batch" class="tab-content">
<div class="content-wrapper">
<div class="section">
<h2>🔄 Batch Processing</h2>
<p style="color: #666; margin-bottom: 20px;">Predict nhiều khu vực cùng lúc</p>
</div>
<div class="alert alert-info">
<span style="font-size: 1.5em;"></span>
<div>
<strong>CSV Format:</strong> name,min_lon,min_lat,max_lon,max_lat,start_date,end_date,max_scenes,cloud_cover,resolution
<br>
<strong>File mẫu:</strong> batch_regions_example.csv
</div>
</div>
<div class="grid-2">
<div class="section">
<h3>📁 Upload CSV</h3>
<div class="form-group">
<label>Chọn file CSV:</label>
<input type="file" id="batchCSVFile" accept=".csv" onchange="handleBatchCSV(event)">
</div>
<div class="form-group">
<label>Chọn Model:</label>
<select id="batchModelSelect">
<option value="">Đang tải...</option>
</select>
</div>
<button class="btn btn-primary" onclick="startBatch()">
🚀 Start Batch Prediction
</button>
</div>
<div class="section">
<h3>📊 Queue Status</h3>
<div class="grid-2">
<div class="stat-card">
<div class="value" id="batchQueued">0</div>
<div class="label">⏳ Queued</div>
</div>
<div class="stat-card">
<div class="value" id="batchRunning">0</div>
<div class="label">▶️ Running</div>
</div>
<div class="stat-card">
<div class="value" id="batchCompleted">0</div>
<div class="label">✅ Completed</div>
</div>
<div class="stat-card">
<div class="value" id="batchFailed">0</div>
<div class="label">❌ Failed</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top: 30px;">
<h3>📋 Jobs List</h3>
<div id="batchJobsList"></div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>🛰️ Land Classification System v2.0 | Powered by Sentinel-2/1 & Microsoft Planetary Computer</p>
</div>
</div>
<!-- Scripts -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
let batchCSVData = [];
let refreshInterval = null;
// Tab switching
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
// Load data for specific tabs
if (tabName === 'home') {
loadHomeStats();
} else if (tabName === 'models') {
loadModelsList();
} else if (tabName === 'reports') {
loadReportsList();
} else if (tabName === 'batch') {
loadBatchModels();
loadBatchStatus();
startBatchRefresh();
} else {
stopBatchRefresh();
}
}
// Load home statistics
async function loadHomeStats() {
try {
const response = await fetch('/api/dashboard/statistics');
const data = await response.json();
document.getElementById('homeModelsCount').textContent = data.models.total;
document.getElementById('homePredictionsCount').textContent = data.predictions.total;
document.getElementById('homeReportsCount').textContent = data.reports.total;
} catch (error) {
console.error('Error loading home stats:', error);
}
}
// Load models list
async function loadModelsList() {
const loading = document.getElementById('modelsLoading');
const list = document.getElementById('modelsList');
const tbody = document.getElementById('modelsTableBody');
loading.style.display = 'block';
list.style.display = 'none';
try {
const response = await fetch('/api/models/list');
const data = await response.json();
tbody.innerHTML = '';
data.models.forEach(model => {
const row = document.createElement('tr');
const accuracy = model.info.metrics?.accuracy
? (model.info.metrics.accuracy * 100).toFixed(2) + '%'
: 'N/A';
row.innerHTML = `
<td><strong>${model.filename}</strong></td>
<td>${model.info.model_type || 'N/A'}</td>
<td><span style="color: #4caf50; font-weight: 600;">${accuracy}</span></td>
<td>${new Date(model.created).toLocaleString('vi-VN')}</td>
<td>${model.size_mb} MB</td>
<td>
<button class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9em;"
onclick="window.open('/api/reports/view/training_report_${model.filename.replace('.joblib', '')}.html', '_blank')">
📄 Report
</button>
</td>
`;
tbody.appendChild(row);
});
loading.style.display = 'none';
list.style.display = 'block';
} catch (error) {
console.error('Error loading models:', error);
loading.innerHTML = '<p style="color: #f44336;">❌ Lỗi khi tải danh sách models</p>';
}
}
// Load reports list
async function loadReportsList() {
const loading = document.getElementById('reportsLoading');
const list = document.getElementById('reportsList');
const tbody = document.getElementById('reportsTableBody');
loading.style.display = 'block';
list.style.display = 'none';
try {
const response = await fetch('/api/reports/list');
const data = await response.json();
tbody.innerHTML = '';
data.reports.forEach(report => {
const row = document.createElement('tr');
const typeIcon = report.type === 'training' ? '🎓' : '🗺️';
// Check if this is a batch job
const isBatchJob = report.is_batch_job;
const batchInfo = report.batch_metadata;
let batchLabel = '';
if (isBatchJob && batchInfo) {
const timestamp = batchInfo.batch_timestamp ?
new Date(batchInfo.batch_timestamp).toLocaleString('vi-VN') :
'N/A';
batchLabel = `
<div style="background: #fff3cd; padding: 5px 8px; border-radius: 4px; margin-top: 5px; font-size: 0.85em;">
<strong>🚀 Batch:</strong> ${batchInfo.batch_name || 'N/A'} |
<strong>ID:</strong> ${batchInfo.batch_job_id || 'N/A'}<br>
<strong>Thời gian:</strong> ${timestamp}
</div>
`;
}
row.innerHTML = `
<td>
<strong>${report.filename}</strong>
${isBatchJob ? '<span style="background: #ffc107; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.75em; margin-left: 5px;">BATCH</span>' : ''}
${batchLabel}
</td>
<td>${typeIcon} ${report.type}</td>
<td>${new Date(report.created).toLocaleString('vi-VN')}</td>
<td>${report.size_kb} KB</td>
<td>
<button class="btn btn-primary" style="padding: 8px 16px; font-size: 0.9em;"
onclick="window.open('${report.view_url}', '_blank')">
👁️ Xem
</button>
<button class="btn btn-success" style="padding: 8px 16px; font-size: 0.9em; margin-left: 5px;"
onclick="window.location.href='${report.download_url}'">
💾 Download
</button>
</td>
`;
if (isBatchJob) {
row.style.borderLeft = '4px solid #ffc107';
}
tbody.appendChild(row);
});
loading.style.display = 'none';
list.style.display = 'block';
} catch (error) {
console.error('Error loading reports:', error);
loading.innerHTML = '<p style="color: #f44336;">❌ Lỗi khi tải danh sách reports</p>';
}
}
// Batch processing functions
async function loadBatchModels() {
try {
const response = await fetch('/api/models/list');
const data = await response.json();
const select = document.getElementById('batchModelSelect');
select.innerHTML = '<option value="">Chọn model...</option>';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.filename;
option.textContent = `${model.filename} (${model.created})`;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading batch models:', error);
}
}
function handleBatchCSV(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
const lines = text.trim().split('\n');
batchCSVData = [];
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].split(',');
if (parts.length >= 5) {
batchCSVData.push({
name: parts[0].trim(),
min_lon: parseFloat(parts[1]),
min_lat: parseFloat(parts[2]),
max_lon: parseFloat(parts[3]),
max_lat: parseFloat(parts[4]),
start_date: parts[5]?.trim() || "2023-03-01",
end_date: parts[6]?.trim() || "2023-05-31",
max_scenes: parseInt(parts[7]) || 12,
cloud_cover: parseInt(parts[8]) || 30,
resolution: parseInt(parts[9]) || 20
});
}
}
alert(`✅ Đã tải ${batchCSVData.length} khu vực từ CSV`);
};
reader.readAsText(file);
}
async function startBatch() {
const modelFilename = document.getElementById('batchModelSelect').value;
if (!modelFilename) {
alert('❌ Vui lòng chọn model');
return;
}
if (batchCSVData.length === 0) {
alert('❌ Vui lòng upload file CSV trước');
return;
}
try {
const response = await fetch('/api/batch/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_filename: modelFilename,
items: batchCSVData,
auto_retry: true,
max_retries: 3
})
});
const result = await response.json();
alert(`✅ ${result.message}`);
loadBatchStatus();
} catch (error) {
console.error('Error starting batch:', error);
alert('❌ Lỗi khi bắt đầu batch prediction');
}
}
async function loadBatchStatus() {
try {
const response = await fetch('/api/batch/status');
const data = await response.json();
document.getElementById('batchQueued').textContent = data.queue.queued;
document.getElementById('batchRunning').textContent = data.queue.running;
document.getElementById('batchCompleted').textContent = data.queue.completed;
document.getElementById('batchFailed').textContent = data.queue.failed;
// Display jobs
const jobsList = document.getElementById('batchJobsList');
const allJobs = [
...data.jobs.running,
...data.jobs.queued,
...data.jobs.recent_completed.slice(0, 5)
];
if (allJobs.length === 0) {
jobsList.innerHTML = '<p style="text-align: center; color: #666;">Chưa có job nào</p>';
return;
}
jobsList.innerHTML = allJobs.map(job => `
<div class="card" style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<strong>${job.name}</strong>
<span class="status-badge ${job.status}">${job.status.toUpperCase()}</span>
</div>
<p style="color: #666; margin: 10px 0;">
📍 [${job.config.min_lon.toFixed(2)}, ${job.config.min_lat.toFixed(2)}] →
[${job.config.max_lon.toFixed(2)}, ${job.config.max_lat.toFixed(2)}]
</p>
${job.error ? `<p style="color: #f44336;">⚠️ ${job.error}</p>` : ''}
</div>
`).join('');
} catch (error) {
console.error('Error loading batch status:', error);
}
}
function startBatchRefresh() {
if (refreshInterval) return;
refreshInterval = setInterval(loadBatchStatus, 3000);
}
function stopBatchRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// Initialize on page load
window.onload = function() {
loadHomeStats();
};
// Cleanup on page unload
window.onbeforeunload = function() {
stopBatchRefresh();
};
</script>
</body>
</html>