sửa các lỗi tại màn hình predict

This commit is contained in:
Victor Phan
2025-12-22 15:43:23 +07:00
parent f3ff5d81a6
commit 10f92f0e2d
12 changed files with 1452 additions and 101 deletions
+222 -98
View File
@@ -218,6 +218,16 @@ async def ndvi_page():
raise HTTPException(status_code=404, detail="NDVI interface không tồn tại") raise HTTPException(status_code=404, detail="NDVI interface không tồn tại")
@app.get("/reports", response_class=HTMLResponse)
async def reports_page():
"""Serve reports management interface"""
html_file = Path(__file__).parent / "reports_interface.html"
if html_file.exists():
return FileResponse(html_file)
else:
raise HTTPException(status_code=404, detail="Reports interface không tồn tại")
@app.get("/api/config/presets") @app.get("/api/config/presets")
async def get_presets(): async def get_presets():
"""Lấy các preset cấu hình sẵn""" """Lấy các preset cấu hình sẵn"""
@@ -324,7 +334,7 @@ async def clear_cache():
@app.get("/api/cache/info") @app.get("/api/cache/info")
async def get_cache_info(): async def get_cache_info():
"""Lấy thông tin về cache với metadata đầy đủ""" """Lấy thông tin về cache với metadata đầy đủ, tự động xóa cache cũ có lazy data"""
cache_dir = Path("dataset_cache") cache_dir = Path("dataset_cache")
if not cache_dir.exists(): if not cache_dir.exists():
@@ -332,16 +342,37 @@ async def get_cache_info():
cache_files = [] cache_files = []
total_size = 0 total_size = 0
deleted_count = 0
for cache_file in cache_dir.glob("*.joblib"): for cache_file in cache_dir.glob("*.joblib"):
size = cache_file.stat().st_size # Skip if file doesn't exist (race condition)
total_size += size if not cache_file.exists():
continue
# Try to load metadata from cache size = cache_file.stat().st_size
# Try to load metadata from cache and check if it's valid
metadata = {} metadata = {}
is_valid = True
try: try:
cached_data = joblib.load(cache_file) cached_data = joblib.load(cache_file)
if isinstance(cached_data, dict):
# Check if cache contains lazy data (will cause 403 errors)
if isinstance(cached_data, dict) and "s2_data" in cached_data:
s2_data_temp = cached_data["s2_data"]
is_lazy = False
try:
is_lazy = any(hasattr(s2_data_temp[var].data, 'chunks') for var in s2_data_temp.data_vars)
except:
pass
if is_lazy:
print(f"[CLEANUP] Deleting cache with lazy data: {cache_file.name}")
cache_file.unlink()
deleted_count += 1
is_valid = False
if is_valid and isinstance(cached_data, dict):
metadata = { metadata = {
"bbox": cached_data.get("bbox", []), "bbox": cached_data.get("bbox", []),
"time_range": cached_data.get("time_range", ""), "time_range": cached_data.get("time_range", ""),
@@ -362,23 +393,36 @@ async def get_cache_info():
metadata["max_lon"] = metadata["bbox"][2] metadata["max_lon"] = metadata["bbox"][2]
metadata["max_lat"] = metadata["bbox"][3] metadata["max_lat"] = metadata["bbox"][3]
except Exception as e: except Exception as e:
print(f"Error loading cache metadata: {e}") print(f"[CLEANUP] Error loading cache {cache_file.name}: {e}. Deleting...")
try:
cache_file.unlink()
deleted_count += 1
is_valid = False
except:
pass
cache_files.append({ # Only add valid cache files to the list
"filename": cache_file.name, if is_valid:
"size_mb": round(size / 1024 / 1024, 2), total_size += size
"modified": datetime.fromtimestamp(cache_file.stat().st_mtime).isoformat(), cache_files.append({
"metadata": metadata "filename": cache_file.name,
}) "size_mb": round(size / 1024 / 1024, 2),
"modified": datetime.fromtimestamp(cache_file.stat().st_mtime).isoformat(),
"metadata": metadata
})
# Sort by modified time (newest first) # Sort by modified time (newest first)
cache_files.sort(key=lambda x: x["modified"], reverse=True) cache_files.sort(key=lambda x: x["modified"], reverse=True)
if deleted_count > 0:
print(f"[CLEANUP] Deleted {deleted_count} invalid cache files")
return { return {
"exists": True, "exists": True,
"files": cache_files, "files": cache_files,
"count": len(cache_files), "count": len(cache_files),
"total_size_mb": round(total_size / 1024 / 1024, 2) "total_size_mb": round(total_size / 1024 / 1024, 2),
"deleted_invalid": deleted_count
} }
@@ -717,15 +761,44 @@ async def run_prediction(config: PredictionConfig):
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat] bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
time_range = f"{config.start_date}/{config.end_date}" time_range = f"{config.start_date}/{config.end_date}"
# Try to load from cache first
s2_data = None
use_cache = False
if cache_file.exists(): if cache_file.exists():
prediction_status["progress"] = "Đang load dữ liệu từ cache..." prediction_status["progress"] = "Đang load dữ liệu từ cache..."
cached = joblib.load(cache_file) try:
s2_data = cached["s2_data"] cached = joblib.load(cache_file)
s2_items = cached["s2_items"] s2_data_temp = cached["s2_data"]
vh_monthly = cached.get("vh_monthly")
vv_monthly = cached.get("vv_monthly") # Verify that cached data is not lazy (to avoid 403 errors from expired URLs)
use_radar = cached.get("use_radar", False) # If s2_data has chunks attribute, it's a dask array (lazy)
else: is_lazy = False
try:
is_lazy = any(hasattr(s2_data_temp[var].data, 'chunks') for var in s2_data_temp.data_vars)
except:
pass
if is_lazy:
print(f"[WARNING] Cache contains lazy data with potentially expired URLs. Deleting cache...")
cache_file.unlink()
raise ValueError("Cache invalid - contains lazy data")
# Cache is valid, use it
s2_data = s2_data_temp
s2_items = cached.get("s2_items", [])
vh_monthly = cached.get("vh_monthly")
vv_monthly = cached.get("vv_monthly")
use_radar = cached.get("use_radar", False)
use_cache = True
print(f"[INFO] Loaded valid cache from {cache_file.name}")
except Exception as e:
print(f"[WARNING] Failed to load cache: {e}. Fetching fresh data...")
s2_data = None
# If cache not available or invalid, fetch from Microsoft
if s2_data is None:
prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..." prediction_status["progress"] = "Đang kết nối Microsoft Planetary Computer..."
import pystac_client import pystac_client
import planetary_computer import planetary_computer
@@ -747,13 +820,17 @@ async def run_prediction(config: PredictionConfig):
raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này") raise ValueError("Không tìm thấy dữ liệu Sentinel-2 cho khu vực và thời gian này")
s2_items = s2_items[:config.max_scenes] s2_items = s2_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..." prediction_status["progress"] = f"Đang xử lý {len(s2_items)} scenes Sentinel-2..."
s2_data = load( s2_data_lazy = load(
s2_items, s2_items,
bbox=bbox, bbox=bbox,
chunks={"time": 1, "x": 2048, "y": 2048}, chunks={"time": 1, "x": 2048, "y": 2048},
groupby="solar_day", groupby="solar_day",
resolution=config.resolution resolution=config.resolution
) )
# Compute s2_data to load into memory (avoid lazy loading from expired URLs)
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-2 vào bộ nhớ..."
s2_data = s2_data_lazy.compute()
# ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============ # ============ BƯỚC 4: TẢI DỮ LIỆU SENTINEL-1 (Radar)... ============
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..." prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
s1_search = catalog.search( s1_search = catalog.search(
@@ -828,59 +905,65 @@ async def run_prediction(config: PredictionConfig):
# Only load radar if not already in cache # Only load radar if not already in cache
if not cache_file.exists() or (cache_file.exists() and not use_radar): if not cache_file.exists() or (cache_file.exists() and not use_radar):
prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..." prediction_status["progress"] = "Đang tải dữ liệu Sentinel-1 (Radar)..."
try:
# Initialize catalog if not already done # Initialize catalog if not already done
if not cache_file.exists(): if not cache_file.exists():
# catalog already initialized in the else block above pass
pass
else:
# Need to initialize catalog for radar search
import pystac_client
import planetary_computer
from odc.stac import load
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
# Search Sentinel-1 data
s1_search = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
s1_items = list(s1_search.items())
if s1_items:
s1_items = s1_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
# Load Sentinel-1 data (without like= to avoid conflict with bbox/resolution)
s1_data = load(
s1_items,
bbox=bbox,
chunks={"time": 1, "x": 2048, "y": 2048},
groupby="sat:absolute_orbit",
resolution=config.resolution
)
# Extract VH and VV bands
if "vh" in s1_data and "vv" in s1_data:
vh = s1_data["vh"].astype('float32')
vv = s1_data["vv"].astype('float32')
# Resample to monthly average
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
vh_monthly = vh.resample(time="1ME").mean().compute()
vv_monthly = vv.resample(time="1ME").mean().compute()
use_radar = True
else: else:
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..." import pystac_client
import planetary_computer
from odc.stac import load
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
# Search Sentinel-1 data
s1_search = catalog.search(
collections=["sentinel-1-rtc"],
bbox=bbox,
datetime=time_range,
)
s1_items = list(s1_search.items())
if s1_items:
s1_items = s1_items[:config.max_scenes]
prediction_status["progress"] = f"Đang xử lý {len(s1_items)} scenes Sentinel-1..."
try:
s1_data = load(
s1_items,
bbox=bbox,
chunks={"time": 1, "x": 2048, "y": 2048},
groupby="sat:absolute_orbit",
resolution=config.resolution
)
if "vh" in s1_data and "vv" in s1_data:
vh = s1_data["vh"].astype('float32')
vv = s1_data["vv"].astype('float32')
prediction_status["progress"] = "Đang tính trung bình VH/VV theo tháng..."
try:
vh_monthly = vh.resample(time="1ME").mean().compute()
vv_monthly = vv.resample(time="1ME").mean().compute()
use_radar = True
except Exception as radar_exc:
print(f"[RADAR WARNING] Không thể tính radar monthly: {radar_exc}")
vh_monthly = None
vv_monthly = None
use_radar = False
else:
prediction_status["progress"] = "Không tìm thấy bands VH/VV, tiếp tục với NDVI..."
use_radar = False
except Exception as radar_exc:
print(f"[RADAR WARNING] Không thể tải dữ liệu Sentinel-1: {radar_exc}")
vh_monthly = None
vv_monthly = None
use_radar = False
else:
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..."
use_radar = False use_radar = False
else: except Exception as radar_exc:
prediction_status["progress"] = "Không có dữ liệu Sentinel-1, tiếp tục với NDVI..." print(f"[RADAR WARNING] Không thể truy cập Sentinel-1: {radar_exc}")
prediction_status["progress"] = "Không thể truy cập Sentinel-1, tiếp tục với NDVI..."
vh_monthly = None
vv_monthly = None
use_radar = False use_radar = False
# ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============ # ============ BƯỚC 5: CHUẨN BỊ FEATURES CHO DỰ ĐOÁN ============
@@ -1776,46 +1859,87 @@ async def predict_with_ndvi(config: PredictionWithNDVIConfig, background_tasks:
import odc.stac import odc.stac
import rasterio import rasterio
from rasterio.transform import from_bounds from rasterio.transform import from_bounds
import hashlib, os
# Load model # Load model
model_path = Path(f"model_train/{config.model_filename}") model_path = Path(f"model_train/{config.model_filename}")
if not model_path.exists(): if not model_path.exists():
raise HTTPException(status_code=404, detail=f"Model {config.model_filename} không tồn tại") raise HTTPException(status_code=404, detail=f"Model {config.model_filename} không tồn tại")
model = joblib.load(model_path) model_data = joblib.load(model_path)
# Extract model from dict (models are saved as {'model': xgb_model, 'label_encoder': encoder})
if isinstance(model_data, dict):
model = model_data.get('model')
label_encoder = model_data.get('label_encoder')
else:
model = model_data
label_encoder = None
print(f"[PREDICT+NDVI] Loaded model: {config.model_filename}") print(f"[PREDICT+NDVI] Loaded model: {config.model_filename}")
# Connect to Microsoft Planetary Computer # Check cache first
catalog = Client.open( cache_dir = Path("dataset_cache")
"https://planetarycomputer.microsoft.com/api/stac/v1", cache_dir.mkdir(exist_ok=True)
modifier=planetary_computer.sign_inplace cache_key = f"pred_{config.min_lon}_{config.min_lat}_{config.max_lon}_{config.max_lat}_{config.start_date}_{config.end_date}_{config.max_scenes}_{config.cloud_cover}_{config.resolution}"
) cache_hash = hashlib.md5(cache_key.encode()).hexdigest()
cache_file = cache_dir / f"prediction_input_{cache_hash}.joblib"
bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat] bbox = [config.min_lon, config.min_lat, config.max_lon, config.max_lat]
time_range = f"{config.start_date}/{config.end_date}"
# Search for Sentinel-2 data # Load from cache or fetch from Microsoft
search = catalog.search( if cache_file.exists():
collections=["sentinel-2-l2a"], print(f"[PREDICT+NDVI] Loading from cache: {cache_file.name}")
bbox=bbox, cached = joblib.load(cache_file)
datetime=time_range, # Extract s2_data from cache (already computed in cache)
query={"eo:cloud_cover": {"lt": config.cloud_cover}} s2_data = cached["s2_data"]
) # Check if needed bands are available in cache
available_bands = list(s2_data.data_vars.keys())
needed_bands = ["B02", "B03", "B04", "B08"]
if all(band in available_bands for band in needed_bands):
# Use cached data directly (no need to compute again)
data = s2_data[needed_bands]
print(f"[PREDICT+NDVI] Using cached bands: {needed_bands}")
# Set items to match the number of time slices in the cached data
items = [None] * s2_data.sizes.get("time", 1)
else:
raise HTTPException(status_code=400,
detail=f"Cache thiếu bands cần thiết. Có: {available_bands}, Cần: {needed_bands}")
else:
print(f"[PREDICT+NDVI] No cache found, fetching from Microsoft Planetary Computer")
# Connect to Microsoft Planetary Computer
catalog = Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace
)
items = list(search.items())[:config.max_scenes] time_range = f"{config.start_date}/{config.end_date}"
print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
if len(items) == 0: # Search for Sentinel-2 data
raise HTTPException(status_code=404, detail="Không tìm thấy dữ liệu vệ tinh") search = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": config.cloud_cover}}
)
# Load all bands needed for features items = list(search.items())[:config.max_scenes]
data = odc.stac.load( print(f"[PREDICT+NDVI] Found {len(items)} Sentinel-2 scenes")
items,
bbox=bbox, if len(items) == 0:
bands=["B02", "B03", "B04", "B08"], # Blue, Green, Red, NIR raise HTTPException(status_code=404, detail="Không tìm thấy dữ liệu vệ tinh")
resolution=config.resolution,
chunks={"x": 2048, "y": 2048} # Sign items to refresh SAS tokens (keep as pystac.Item, not dict)
).compute() signed_items = [planetary_computer.sign(item) for item in items]
# Load all bands needed for features
data = odc.stac.load(
signed_items,
bbox=bbox,
bands=["B02", "B03", "B04", "B08"], # Blue, Green, Red, NIR
resolution=config.resolution,
chunks={"x": 2048, "y": 2048}
).compute()
print(f"[PREDICT+NDVI] Loaded data shape: {data.dims}") print(f"[PREDICT+NDVI] Loaded data shape: {data.dims}")
+1
View File
@@ -209,6 +209,7 @@
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</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 (Active)</a> <a href="/batch" style="padding: 10px 20px; background: #764ba2; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🚀 Batch Processing (Active)</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="/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> </div>
<div class="content"> <div class="content">
+1
View File
@@ -433,6 +433,7 @@
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</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="/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="/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> </div>
<!-- Tab Content: Home --> <!-- Tab Content: Home -->
+1
View File
@@ -209,6 +209,7 @@
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</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="/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 (Active)</a> <a href="/ndvi" style="padding: 10px 20px; background: #2ecc71; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🌿 NDVI Analysis (Active)</a>
<a href="/reports" style="padding: 10px 20px; background: #ff6b6b; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports</a>
</div> </div>
<div class="content"> <div class="content">
+81
View File
@@ -298,6 +298,7 @@
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction (Active)</a> <a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction (Active)</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="/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="/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> </div>
<!-- Tab Navigation --> <!-- Tab Navigation -->
@@ -957,6 +958,10 @@
const data = await response.json(); const data = await response.json();
const select = document.getElementById('cacheSelect'); const select = document.getElementById('cacheSelect');
select.innerHTML = '<option value="">-- Không dùng cache --</option>'; select.innerHTML = '<option value="">-- Không dùng cache --</option>';
// Store cache data globally for later use
window.cacheFiles = data.files || [];
if (data.files && data.files.length > 0) { if (data.files && data.files.length > 0) {
data.files.forEach((file, idx) => { data.files.forEach((file, idx) => {
if (file.filename.startsWith('prediction_input_')) { if (file.filename.startsWith('prediction_input_')) {
@@ -976,6 +981,80 @@
} }
} }
// Apply cache preset - auto fill bbox and other params
function applyCachePreset() {
const selectValue = document.getElementById('cacheSelect').value;
if (!selectValue || !window.cacheFiles) return;
// Find selected cache file
const cacheFile = window.cacheFiles.find(f => f.filename === selectValue);
if (!cacheFile || !cacheFile.metadata) {
console.warn('No metadata found for selected cache');
return;
}
const meta = cacheFile.metadata;
// Auto-fill bbox from cache
if (meta.bbox && meta.bbox.length === 4) {
const [min_lon, min_lat, max_lon, max_lat] = meta.bbox;
// Set selectedBbox directly without drawing on map
selectedBbox = {
min_lon: min_lon,
min_lat: min_lat,
max_lon: max_lon,
max_lat: max_lat
};
// Save to localStorage
localStorage.setItem('prediction_bbox', JSON.stringify(selectedBbox));
// Draw rectangle on map to visualize
if (currentRectangle) {
drawnItems.removeLayer(currentRectangle);
}
const bounds = L.latLngBounds(
[min_lat, min_lon],
[max_lat, max_lon]
);
currentRectangle = L.rectangle(bounds, {
color: '#4facfe',
weight: 3,
fillOpacity: 0.2
});
drawnItems.addLayer(currentRectangle);
map.fitBounds(bounds);
console.log(`✅ Auto-applied bbox from cache: [${min_lon}, ${min_lat}, ${max_lon}, ${max_lat}]`);
}
// Auto-fill time range if available
if (meta.start_date) {
document.getElementById('predStartDate').value = meta.start_date;
}
if (meta.end_date) {
document.getElementById('predEndDate').value = meta.end_date;
}
// Auto-fill other params
if (meta.resolution) {
document.getElementById('predResolution').value = meta.resolution;
}
if (meta.max_scenes) {
document.getElementById('predMaxScenes').value = meta.max_scenes;
}
if (meta.cloud_cover) {
document.getElementById('predCloudCover').value = meta.cloud_cover;
}
// Show notification
alert(`✅ Đã áp dụng cache preset!\n\nBBox: [${meta.bbox?.join(', ') || 'N/A'}]\nTime: ${meta.start_date || '?'}${meta.end_date || '?'}\n\nBạn có thể predict ngay mà không cần vẽ bbox!`);
}
// Initialize on page load // Initialize on page load
window.onload = function() { window.onload = function() {
initMap(); initMap();
@@ -984,6 +1063,8 @@
loadCacheList(); loadCacheList();
// Add event listener for model selection // Add event listener for model selection
document.getElementById('modelSelect').addEventListener('change', updateModelInfo); document.getElementById('modelSelect').addEventListener('change', updateModelInfo);
// Add event listener for cache selection
document.getElementById('cacheSelect').addEventListener('change', applyCachePreset);
}; };
// Cleanup on page unload // Cleanup on page unload
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251222_115713</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 22/12/2025 11:57:13</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">165</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">11x15</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.1</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_xgboost_20251221_172351.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.47426033018384, 9.250032954766686, 105.47683525083814, 9.251917848893436]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251222_115712.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 22/12/2025 11:57:13</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251222_153138</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 22/12/2025 15:31:38</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">165</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">11x15</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.1</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_xgboost_20251221_172351.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.47426033018384, 9.250032954766686, 105.47683525083814, 9.251917848893436]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251222_153137.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 22/12/2025 15:31:38</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251222_154215</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 22/12/2025 15:42:15</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">1,053</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">27x39</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.3</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">2</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_xgboost_20251221_172351.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.54108810418259, 9.340180964398723, 105.54791164391646, 9.344839034909683]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251222_154215.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">3</span><span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 22/12/2025 15:42:15</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prediction Report - 20251222_154237</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #ff6b6b;
border-bottom: 3px solid #ff6b6b;
padding-bottom: 10px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: linear-gradient(135deg, #ff6b6b15 0%, #ee5a6f15 100%);
padding: 25px;
border-radius: 10px;
text-align: center;
border: 1px solid #ff6b6b30;
}
.stat-card .value {
font-size: 2em;
font-weight: bold;
color: #ff6b6b;
}
.stat-card .label {
color: #666;
margin-top: 5px;
}
.info-box {
background: #fff3cd;
padding: 20px;
border-radius: 10px;
border-left: 5px solid #ff6b6b;
margin: 20px 0;
}
.info-row {
display: flex;
margin: 10px 0;
}
.info-label {
font-weight: bold;
width: 200px;
color: #555;
}
.class-badge {
display: inline-block;
background: #ff6b6b;
color: white;
padding: 8px 15px;
border-radius: 20px;
margin: 5px;
}
.footer {
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ Báo Cáo Dự Đoán</h1>
<p>Land Classification Prediction - 22/12/2025 15:42:37</p>
</div>
<div class="content">
<div class="section">
<h2>📈 Tóm Tắt Kết Quả</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="value">1,053</div>
<div class="label">Tổng số Pixels</div>
</div>
<div class="stat-card">
<div class="value">27x39</div>
<div class="label">Kích thước (px)</div>
</div>
<div class="stat-card">
<div class="value">0.3</div>
<div class="label">Diện tích (km²)</div>
</div>
<div class="stat-card">
<div class="value">1</div>
<div class="label">Số Classes</div>
</div>
<div class="stat-card">
<div class="value">3</div>
<div class="label">Số Features</div>
</div>
<div class="stat-card">
<div class="value"></div>
<div class="label">Sử dụng Radar</div>
</div>
</div>
</div>
<div class="section">
<h2>⚙️ Thông Tin Chi Tiết</h2>
<div class="info-box">
<div class="info-row">
<span class="info-label">🤖 Model sử dụng:</span>
<span>model_cnn_20251221_163841.joblib</span>
</div>
<div class="info-row">
<span class="info-label">📍 Khu vực (bbox):</span>
<span>[105.54108810418259, 9.340180964398723, 105.54791164391646, 9.344839034909683]</span>
</div>
<div class="info-row">
<span class="info-label">📅 Thời gian:</span>
<span>2023-03-01/2023-05-31</span>
</div>
<div class="info-row">
<span class="info-label">💾 Output file:</span>
<span>predictions/prediction_20251222_154237.tif</span>
</div>
</div>
</div>
<div class="section">
<h2>🏷️ Các Classes Phát Hiện</h2>
<div>
<span class="class-badge">6</span>
</div>
</div>
</div>
<div class="footer">
<p>🌍 Land Classification System | Generated: 22/12/2025 15:42:37</p>
</div>
</div>
</body>
</html>
+436
View File
@@ -0,0 +1,436 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reports Management</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 10px;
}
.section h2 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.5em;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 5px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-right: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.reports-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-top: 20px;
}
.report-card {
background: white;
padding: 20px;
border-radius: 10px;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.3s;
}
.report-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
}
.report-card h3 {
color: #667eea;
margin-bottom: 10px;
font-size: 1.1em;
}
.report-card .meta {
color: #666;
font-size: 0.9em;
margin-bottom: 15px;
}
.report-card .badge {
display: inline-block;
padding: 5px 12px;
border-radius: 15px;
font-size: 0.85em;
font-weight: 600;
margin-bottom: 10px;
}
.badge-training {
background: #667eea;
color: white;
}
.badge-prediction {
background: #ff6b6b;
color: white;
}
.badge-batch {
background: #feca57;
color: #333;
}
.report-card .actions {
display: flex;
gap: 8px;
margin-top: 15px;
}
.report-card .btn {
padding: 8px 15px;
font-size: 0.9em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #667eea;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-card h3 {
color: #666;
font-size: 0.9em;
margin-bottom: 10px;
}
.stat-card .value {
color: #667eea;
font-size: 2em;
font-weight: bold;
}
.filter-section {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-btn {
padding: 10px 20px;
background: white;
border: 2px solid #667eea;
color: #667eea;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn:hover,
.filter-btn.active {
background: #667eea;
color: white;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state i {
font-size: 4em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📝 Reports Management</h1>
<p>Quản lý báo cáo training và prediction</p>
</div>
<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ủ</a>
<a href="/training" style="padding: 10px 20px; background: #4facfe; 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: #f093fb; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">📝 Reports (Active)</a>
</div>
<div class="content">
<!-- Statistics -->
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<h3>📊 Total Reports</h3>
<div class="value" id="statTotal">0</div>
</div>
<div class="stat-card" style="border-left-color: #667eea;">
<h3>🎓 Training Reports</h3>
<div class="value" id="statTraining" style="color: #667eea;">0</div>
</div>
<div class="stat-card" style="border-left-color: #ff6b6b;">
<h3>🗺️ Prediction Reports</h3>
<div class="value" id="statPrediction" style="color: #ff6b6b;">0</div>
</div>
<div class="stat-card" style="border-left-color: #feca57;">
<h3>🚀 Batch Reports</h3>
<div class="value" id="statBatch" style="color: #feca57;">0</div>
</div>
</div>
<!-- Filters -->
<div class="filter-section">
<button class="filter-btn active" onclick="filterReports('all')">Tất cả</button>
<button class="filter-btn" onclick="filterReports('training')">Training</button>
<button class="filter-btn" onclick="filterReports('prediction')">Prediction</button>
<button class="filter-btn" onclick="filterReports('batch')">Batch Jobs</button>
<button class="btn btn-secondary" onclick="loadReports()" style="margin-left: auto;">🔄 Refresh</button>
</div>
<!-- Reports Grid -->
<div class="section">
<div id="reportsGrid" class="reports-grid">
<div class="empty-state">
<p>⏳ Đang tải...</p>
</div>
</div>
</div>
</div>
</div>
<script>
const API_BASE = 'http://localhost:8000/api';
let allReports = [];
let currentFilter = 'all';
// Load reports on page load
document.addEventListener('DOMContentLoaded', () => {
loadReports();
});
async function loadReports() {
try {
const response = await fetch(`${API_BASE}/reports/list`);
const data = await response.json();
allReports = data.reports;
updateStats(data.reports);
displayReports(filterReportsByType(data.reports, currentFilter));
} catch (error) {
console.error('Error loading reports:', error);
document.getElementById('reportsGrid').innerHTML = `
<div class="empty-state">
<p style="color: red;">❌ Lỗi khi tải reports: ${error.message}</p>
</div>
`;
}
}
function updateStats(reports) {
const total = reports.length;
const training = reports.filter(r => r.type === 'training').length;
const prediction = reports.filter(r => r.type === 'prediction').length;
const batch = reports.filter(r => r.is_batch_job).length;
document.getElementById('statTotal').textContent = total;
document.getElementById('statTraining').textContent = training;
document.getElementById('statPrediction').textContent = prediction;
document.getElementById('statBatch').textContent = batch;
}
function filterReports(type) {
currentFilter = type;
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Filter and display
const filtered = filterReportsByType(allReports, type);
displayReports(filtered);
}
function filterReportsByType(reports, type) {
if (type === 'all') return reports;
if (type === 'batch') return reports.filter(r => r.is_batch_job);
return reports.filter(r => r.type === type);
}
function displayReports(reports) {
const grid = document.getElementById('reportsGrid');
if (reports.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<p>📝 Không có báo cáo nào</p>
</div>
`;
return;
}
grid.innerHTML = reports.map(report => {
const badgeClass = report.type === 'training' ? 'badge-training' : 'badge-prediction';
const badgeText = report.type === 'training' ? '🎓 Training' : '🗺️ Prediction';
const batchBadge = report.is_batch_job ? '<span class="badge badge-batch">🚀 Batch Job</span>' : '';
const createdDate = new Date(report.created).toLocaleString('vi-VN');
let metaInfo = `
<p>📅 ${createdDate}</p>
<p>💾 ${report.size_kb} KB</p>
`;
if (report.batch_metadata) {
metaInfo += `
<p style="margin-top: 5px; font-weight: 600;">
📦 ${report.batch_metadata.batch_name || 'Batch Job'}
</p>
`;
}
return `
<div class="report-card">
<span class="badge ${badgeClass}">${badgeText}</span>
${batchBadge}
<h3>📄 ${report.filename}</h3>
<div class="meta">
${metaInfo}
</div>
<div class="actions">
<button class="btn btn-primary" onclick="viewReport('${report.filename}')">
👁️ Xem
</button>
<button class="btn btn-success" onclick="downloadReport('${report.filename}')">
💾 Tải
</button>
<button class="btn btn-danger" onclick="deleteReport('${report.filename}')">
🗑️ Xóa
</button>
</div>
</div>
`;
}).join('');
}
function viewReport(filename) {
window.open(`${API_BASE}/reports/view/${filename}`, '_blank');
}
function downloadReport(filename) {
window.location.href = `${API_BASE}/reports/download/${filename}`;
}
async function deleteReport(filename) {
if (!confirm(`Bạn có chắc muốn xóa báo cáo: ${filename}?`)) {
return;
}
try {
const response = await fetch(`${API_BASE}/reports/delete/${filename}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
alert('✅ Đã xóa báo cáo thành công!');
loadReports();
} else {
alert('❌ Không thể xóa báo cáo!');
}
} catch (error) {
console.error('Error deleting report:', error);
alert('❌ Lỗi khi xóa báo cáo: ' + error.message);
}
}
</script>
</body>
</html>
+3 -1
View File
@@ -1 +1,3 @@
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
#uvicorn api_server:app --reload --host 0.0.0.0 --port 8000
pkill -f "uvicorn api_server:app" && sleep 1 && nohup uvicorn api_server:app --host 0.0.0.0 --port 8000 > server.log 2>&1 &
+1
View File
@@ -296,6 +296,7 @@
<a href="/prediction" style="padding: 10px 20px; background: #4facfe; color: white; border-radius: 8px; text-decoration: none; font-weight: 600;">🗺️ Prediction</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="/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="/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> </div>
<div class="content"> <div class="content">