web: update web interface
Local Docker Build (Dev) / build-dev (push) Successful in 8s

This commit is contained in:
2025-10-28 15:07:03 -04:00
parent 1916dc2646
commit afdbe1f755
6 changed files with 610 additions and 1469 deletions
+58 -1
View File
@@ -842,4 +842,61 @@ body {
.d-block { display: block; }
.d-flex { display: flex; }
.justify-content-between { justify-content: space-between; }
.align-items-center { align-items: center; }
.align-items-center { align-items: center; }
/* Manual Scan Styles */
.scan-status {
margin-top: 1rem;
padding: 1rem;
background-color: var(--light-color);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
.scan-progress {
margin-bottom: 1rem;
}
.progress-bar {
width: 100%;
height: 1.5rem;
background-color: #e9ecef;
border-radius: 0.375rem;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--success-color));
transition: width 0.3s ease;
width: 0%;
}
.scan-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
}
.scan-info span:first-child {
color: var(--text-muted);
}
.scan-info span:last-child {
font-weight: 600;
color: var(--primary-color);
}
.form-group small {
display: block;
margin-top: 0.25rem;
color: var(--text-muted);
font-size: 0.875rem;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
+47 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFOGuard - Database Management</title>
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/styles.css?v=manual-scan-ui">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
@@ -327,6 +327,51 @@
</form>
</div>
<div class="tool-card">
<h3><i class="fas fa-search"></i> Manual Scan</h3>
<p>Scan specific folders or perform full library scans</p>
<form id="manual-scan-form">
<div class="form-group">
<label>Scan Type:</label>
<select id="scan-type" required>
<option value="both">TV Shows & Movies</option>
<option value="tv">TV Shows Only</option>
<option value="movies">Movies Only</option>
</select>
</div>
<div class="form-group">
<label>Scan Mode:</label>
<select id="scan-mode" required>
<option value="smart">Smart (Recommended)</option>
<option value="full">Full Scan</option>
<option value="incomplete">Incomplete Only</option>
</select>
</div>
<div class="form-group">
<label>Specific Path (Optional):</label>
<input type="text" id="scan-path" placeholder="e.g., /mnt/unionfs/Media/TV/Series Name" title="Leave empty for full library scan">
<small>Leave empty to scan entire library</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-play"></i> Start Scan
</button>
</form>
<div id="scan-status" class="scan-status" style="display: none;">
<div class="scan-progress">
<div class="progress-bar">
<div class="progress-fill" id="scan-progress-bar"></div>
</div>
<div class="scan-info">
<span id="scan-current-operation">Initializing...</span>
<span id="scan-progress-text">0%</span>
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="stopScanPolling()">
<i class="fas fa-times"></i> Hide Status
</button>
</div>
</div>
<div class="tool-card">
<h3><i class="fas fa-database"></i> Database Statistics</h3>
<p>View detailed database information</p>
@@ -404,6 +449,6 @@
<!-- Toast Notifications -->
<div class="toast-container" id="toast-container"></div>
<script src="/static/js/app.js"></script>
<script src="/static/js/app.js?v=manual-scan-ui"></script>
</body>
</html>
+164
View File
@@ -76,6 +76,7 @@ function initializeEventListeners() {
// Forms
document.getElementById('edit-form').addEventListener('submit', handleEditSubmit);
document.getElementById('bulk-update-form').addEventListener('submit', handleBulkUpdate);
document.getElementById('manual-scan-form').addEventListener('submit', handleManualScan);
}
// API calls
@@ -1436,6 +1437,169 @@ async function checkAuthStatus() {
}
}
// Manual Scan Functions
async function handleManualScan(event) {
event.preventDefault();
const scanType = document.getElementById('scan-type').value;
const scanMode = document.getElementById('scan-mode').value;
const scanPath = document.getElementById('scan-path').value.trim();
// Validate inputs
if (!scanType || !scanMode) {
showToast('❌ Please select scan type and mode', 'error');
return;
}
// Build query parameters
const params = new URLSearchParams({
scan_type: scanType,
scan_mode: scanMode
});
if (scanPath) {
params.append('path', scanPath);
}
try {
// Show scan status
showScanStatus();
// Start the scan
showToast('🚀 Starting manual scan...', 'info');
const response = await fetch(`/manual/scan?${params}`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.status === 'started') {
showToast('✅ Scan started successfully', 'success');
// Start polling for status
startScanPolling();
} else {
showToast(`${result.message || 'Scan completed'}`, 'info');
hideScanStatus();
}
} catch (error) {
console.error('Manual scan failed:', error);
showToast(`❌ Scan failed: ${error.message}`, 'error');
hideScanStatus();
}
}
function showScanStatus() {
const scanStatus = document.getElementById('scan-status');
const progressBar = document.getElementById('scan-progress-bar');
const operationText = document.getElementById('scan-current-operation');
const progressText = document.getElementById('scan-progress-text');
// Reset and show
progressBar.style.width = '0%';
operationText.textContent = 'Initializing scan...';
progressText.textContent = '0%';
scanStatus.style.display = 'block';
}
function hideScanStatus() {
document.getElementById('scan-status').style.display = 'none';
if (window.scanPollingInterval) {
clearInterval(window.scanPollingInterval);
window.scanPollingInterval = null;
}
}
function stopScanPolling() {
hideScanStatus();
}
function startScanPolling() {
// Poll every 2 seconds for scan status
window.scanPollingInterval = setInterval(async () => {
try {
const response = await fetch('/api/scan/status');
if (!response.ok) {
throw new Error('Failed to get scan status');
}
const status = await response.json();
updateScanProgress(status);
// Stop polling if scan is complete
if (!status.scanning) {
stopScanPolling();
showToast('✅ Scan completed!', 'success');
}
} catch (error) {
console.error('Failed to poll scan status:', error);
// Don't show error toast repeatedly, just stop polling
stopScanPolling();
}
}, 2000);
}
function updateScanProgress(status) {
const progressBar = document.getElementById('scan-progress-bar');
const operationText = document.getElementById('scan-current-operation');
const progressText = document.getElementById('scan-progress-text');
if (!status.scanning) {
progressBar.style.width = '100%';
operationText.textContent = 'Scan completed';
progressText.textContent = '100%';
return;
}
// Calculate overall progress
let totalProgress = 0;
let progressDetails = '';
if (status.scan_type === 'both' || status.scan_type === 'tv') {
const tvProgress = status.tv_series_total > 0 ?
((status.tv_series_processed + status.tv_series_skipped) / status.tv_series_total) * 50 : 0;
totalProgress += tvProgress;
if (status.tv_series_total > 0) {
progressDetails += `TV: ${status.tv_series_processed + status.tv_series_skipped}/${status.tv_series_total} `;
}
}
if (status.scan_type === 'both' || status.scan_type === 'movies') {
const movieProgress = status.movies_total > 0 ?
((status.movies_processed + status.movies_skipped) / status.movies_total) * 50 : 0;
totalProgress += movieProgress;
if (status.movies_total > 0) {
progressDetails += `Movies: ${status.movies_processed + status.movies_skipped}/${status.movies_total}`;
}
}
// For single type scans, use full 100%
if (status.scan_type !== 'both') {
totalProgress *= 2;
}
// Update progress bar
progressBar.style.width = `${Math.min(totalProgress, 100)}%`;
progressText.textContent = `${Math.round(totalProgress)}%`;
// Update operation text
if (status.current_operation) {
operationText.textContent = status.current_operation;
} else if (status.current_item) {
operationText.textContent = `Processing: ${status.current_item}`;
} else {
operationText.textContent = progressDetails || 'Scanning...';
}
}
async function logout() {
if (!confirm('Are you sure you want to logout?')) {
return;