initial comment
This commit is contained in:
413
static/js/dashboard.js
Normal file
413
static/js/dashboard.js
Normal file
@@ -0,0 +1,413 @@
|
||||
// Dashboard State
|
||||
let isProcessing = false;
|
||||
let currentConfig = {};
|
||||
let currentProfiles = {};
|
||||
let editingProfileName = null;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadConfiguration();
|
||||
loadProfiles();
|
||||
refreshData();
|
||||
setInterval(refreshData, 5000);
|
||||
});
|
||||
|
||||
// Tab Management
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
document.getElementById(tabName + '-tab').classList.add('active');
|
||||
|
||||
if (tabName === 'settings') loadConfiguration();
|
||||
if (tabName === 'profiles') loadProfiles();
|
||||
}
|
||||
|
||||
// Data Refresh
|
||||
async function refreshData() {
|
||||
await Promise.all([
|
||||
updateStats(),
|
||||
updateSystemStats(),
|
||||
updateActivity(),
|
||||
updateLogs(),
|
||||
checkProcessingStatus()
|
||||
]);
|
||||
}
|
||||
|
||||
// Configuration Management
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
currentConfig = result.data;
|
||||
document.getElementById('movies_dir').value = result.data.movies_dir || '';
|
||||
document.getElementById('archive_dir').value = result.data.archive_dir || '';
|
||||
document.getElementById('work_dir').value = result.data.work_dir || '';
|
||||
if (result.data.parallel) {
|
||||
document.getElementById('max_workers').value = result.data.parallel.max_workers || 2;
|
||||
document.getElementById('gpu_slots').value = result.data.parallel.gpu_slots || 1;
|
||||
document.getElementById('cpu_slots').value = result.data.parallel.cpu_slots || 4;
|
||||
}
|
||||
if (result.data.processing) {
|
||||
document.getElementById('skip_without_subtitles').checked = result.data.processing.skip_without_subtitles !== false;
|
||||
document.getElementById('cleanup_stale_work').checked = result.data.processing.cleanup_stale_work !== false;
|
||||
}
|
||||
if (result.data.advanced) {
|
||||
document.getElementById('prefer_gpu').checked = result.data.advanced.prefer_gpu !== false;
|
||||
document.getElementById('fallback_to_cpu').checked = result.data.advanced.fallback_to_cpu !== false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load config failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateConfiguration() {
|
||||
const config = gatherConfig();
|
||||
try {
|
||||
const res = await fetch('/api/config/validate', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) displayValidation(result.data);
|
||||
} catch (e) {
|
||||
alert('Validation failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfiguration() {
|
||||
const config = gatherConfig();
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
alert('✓ Configuration saved!');
|
||||
currentConfig = result.data;
|
||||
} else {
|
||||
alert('Failed: ' + result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Save failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function gatherConfig() {
|
||||
return {
|
||||
movies_dir: document.getElementById('movies_dir').value,
|
||||
archive_dir: document.getElementById('archive_dir').value,
|
||||
work_dir: document.getElementById('work_dir').value,
|
||||
state_db: currentConfig.state_db || '/var/lib/reencode/state.db',
|
||||
log_dir: currentConfig.log_dir || '/var/log/reencode',
|
||||
parallel: {
|
||||
max_workers: parseInt(document.getElementById('max_workers').value),
|
||||
gpu_slots: parseInt(document.getElementById('gpu_slots').value),
|
||||
cpu_slots: parseInt(document.getElementById('cpu_slots').value)
|
||||
},
|
||||
processing: {
|
||||
file_extensions: currentConfig.processing?.file_extensions || ['mkv', 'mp4', 'avi', 'm4v'],
|
||||
skip_without_subtitles: document.getElementById('skip_without_subtitles').checked,
|
||||
cleanup_stale_work: document.getElementById('cleanup_stale_work').checked
|
||||
},
|
||||
advanced: {
|
||||
auto_detect_encoders: true,
|
||||
prefer_gpu: document.getElementById('prefer_gpu').checked,
|
||||
fallback_to_cpu: document.getElementById('fallback_to_cpu').checked,
|
||||
progress_interval: 10
|
||||
},
|
||||
profiles: currentConfig.profiles || {}
|
||||
};
|
||||
}
|
||||
|
||||
function displayValidation(data) {
|
||||
let html = '';
|
||||
if (data.errors.length === 0 && data.warnings.length === 0) {
|
||||
html = '<div class="validation-message validation-success">✓ Configuration is valid!</div>';
|
||||
} else {
|
||||
if (data.errors.length > 0) {
|
||||
html += '<div class="validation-message validation-error"><strong>Errors:</strong><ul>';
|
||||
data.errors.forEach(e => html += `<li>${e}</li>`);
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (data.warnings.length > 0) {
|
||||
html += '<div class="validation-message validation-warning"><strong>Warnings:</strong><ul>';
|
||||
data.warnings.forEach(w => html += `<li>${w}</li>`);
|
||||
html += '</ul></div>';
|
||||
}
|
||||
}
|
||||
document.getElementById('validationMessages').innerHTML = html;
|
||||
}
|
||||
|
||||
// Profile Management
|
||||
async function loadProfiles() {
|
||||
try {
|
||||
const res = await fetch('/api/profiles');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
currentProfiles = result.data.profiles;
|
||||
renderProfiles();
|
||||
populateProfileSelects(result.data.default);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load profiles failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderProfiles() {
|
||||
let html = '';
|
||||
for (const [name, profile] of Object.entries(currentProfiles)) {
|
||||
html += `<div class="profile-editor">
|
||||
<div class="profile-header">
|
||||
<strong>${name}</strong>
|
||||
<div>
|
||||
<button class="btn btn-secondary" style="padding: 6px 12px;" onclick="editProfile('${name}')">✏️</button>
|
||||
<button class="btn btn-danger" style="padding: 6px 12px;" onclick="deleteProfile('${name}')">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color: #94a3b8; font-size: 0.9em;">
|
||||
Encoder: ${profile.encoder} | Preset: ${profile.preset} | Quality: ${profile.quality}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
document.getElementById('profilesList').innerHTML = html;
|
||||
}
|
||||
|
||||
function populateProfileSelects(defaultProfile) {
|
||||
let opts = '<option value="">Default</option>';
|
||||
for (const name of Object.keys(currentProfiles)) {
|
||||
opts += `<option value="${name}">${name}</option>`;
|
||||
}
|
||||
document.getElementById('profileSelect').innerHTML = opts;
|
||||
document.getElementById('default_profile').innerHTML = opts.replace('<option value="">Default</option>', '');
|
||||
if (defaultProfile) document.getElementById('default_profile').value = defaultProfile;
|
||||
}
|
||||
|
||||
function addNewProfile() {
|
||||
editingProfileName = null;
|
||||
document.getElementById('modalTitle').textContent = 'Add Profile';
|
||||
document.getElementById('modal_profile_name').value = '';
|
||||
document.getElementById('modal_profile_name').disabled = false;
|
||||
document.getElementById('modal_encoder').value = 'nvidia_nvenc_h265';
|
||||
document.getElementById('modal_preset').value = 'medium';
|
||||
document.getElementById('modal_quality').value = '23';
|
||||
document.getElementById('modal_audio_codec').value = 'copy';
|
||||
document.getElementById('profileModal').classList.add('active');
|
||||
}
|
||||
|
||||
function editProfile(name) {
|
||||
editingProfileName = name;
|
||||
const profile = currentProfiles[name];
|
||||
document.getElementById('modalTitle').textContent = 'Edit: ' + name;
|
||||
document.getElementById('modal_profile_name').value = name;
|
||||
document.getElementById('modal_profile_name').disabled = true;
|
||||
document.getElementById('modal_encoder').value = profile.encoder;
|
||||
document.getElementById('modal_preset').value = profile.preset;
|
||||
document.getElementById('modal_quality').value = profile.quality;
|
||||
document.getElementById('modal_audio_codec').value = profile.audio_codec || 'copy';
|
||||
document.getElementById('profileModal').classList.add('active');
|
||||
}
|
||||
|
||||
function deleteProfile(name) {
|
||||
if (confirm(`Delete "${name}"?`)) {
|
||||
delete currentProfiles[name];
|
||||
renderProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
function closeProfileModal() {
|
||||
document.getElementById('profileModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function saveProfileFromModal() {
|
||||
const name = document.getElementById('modal_profile_name').value.trim();
|
||||
if (!name) return alert('Name required');
|
||||
currentProfiles[name] = {
|
||||
encoder: document.getElementById('modal_encoder').value,
|
||||
preset: document.getElementById('modal_preset').value,
|
||||
quality: parseInt(document.getElementById('modal_quality').value),
|
||||
audio_codec: document.getElementById('modal_audio_codec').value
|
||||
};
|
||||
renderProfiles();
|
||||
populateProfileSelects(currentConfig.profiles?.default);
|
||||
closeProfileModal();
|
||||
}
|
||||
|
||||
async function saveProfiles() {
|
||||
const config = gatherConfig();
|
||||
config.profiles = {
|
||||
default: document.getElementById('default_profile').value,
|
||||
definitions: currentProfiles
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
alert('✓ Profiles saved!');
|
||||
currentConfig = result.data;
|
||||
} else {
|
||||
alert('Failed: ' + result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Save failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Job Control
|
||||
async function startProcessing() {
|
||||
const profile = document.getElementById('profileSelect').value;
|
||||
const dryRun = document.getElementById('dryRunCheckbox').checked;
|
||||
try {
|
||||
const res = await fetch('/api/jobs/start', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({profile: profile || null, dry_run: dryRun})
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
isProcessing = true;
|
||||
updateUIState();
|
||||
setTimeout(refreshData, 1000);
|
||||
} else {
|
||||
alert('Failed: ' + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopProcessing() {
|
||||
if (!confirm('Stop processing?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/jobs/stop', {method: 'POST'});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
isProcessing = false;
|
||||
updateUIState();
|
||||
setTimeout(refreshData, 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkProcessingStatus() {
|
||||
try {
|
||||
const res = await fetch('/api/processing');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
isProcessing = result.data.active;
|
||||
updateUIState();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function updateUIState() {
|
||||
document.getElementById('btnStart').disabled = isProcessing;
|
||||
document.getElementById('btnStop').disabled = !isProcessing;
|
||||
const badge = document.getElementById('statusBadge');
|
||||
if (isProcessing) {
|
||||
badge.textContent = 'Processing';
|
||||
badge.className = 'status-badge status-active';
|
||||
} else {
|
||||
badge.textContent = 'Idle';
|
||||
badge.className = 'status-badge status-idle';
|
||||
}
|
||||
}
|
||||
|
||||
// Stats Updates
|
||||
async function updateStats() {
|
||||
try {
|
||||
const res = await fetch('/api/stats');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
const d = result.data;
|
||||
document.getElementById('statPending').textContent = d.pending || 0;
|
||||
document.getElementById('statProcessing').textContent = d.processing || 0;
|
||||
document.getElementById('statCompleted').textContent = d.completed || 0;
|
||||
document.getElementById('statFailed').textContent = d.failed || 0;
|
||||
document.getElementById('statSkipped').textContent = d.skipped || 0;
|
||||
|
||||
const total = (d.completed || 0) + (d.failed || 0) + (d.pending || 0) + (d.processing || 0);
|
||||
const done = (d.completed || 0) + (d.failed || 0);
|
||||
const prog = total > 0 ? (done / total * 100) : 0;
|
||||
document.getElementById('progressBar').style.width = prog + '%';
|
||||
|
||||
document.getElementById('originalSize').textContent = formatBytes(d.original_size);
|
||||
document.getElementById('encodedSize').textContent = formatBytes(d.encoded_size);
|
||||
document.getElementById('spaceSaved').textContent = formatBytes(d.space_saved) + ' (' + d.space_saved_percent + '%)';
|
||||
document.getElementById('avgFps').textContent = (d.avg_fps || 0).toFixed(1) + ' fps';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function updateSystemStats() {
|
||||
try {
|
||||
const res = await fetch('/api/system');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
let html = '';
|
||||
if (result.data.gpu && result.data.gpu.length > 0) {
|
||||
result.data.gpu.forEach(gpu => {
|
||||
html += `<div class="card" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white;">
|
||||
<h3>🎮 ${gpu.name}</h3>
|
||||
<div style="margin-top: 10px;">
|
||||
GPU: ${gpu.utilization}% | Memory: ${gpu.memory_used}/${gpu.memory_total}MB | Temp: ${gpu.temperature}°C
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
document.getElementById('systemStats').innerHTML = html;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function updateActivity() {
|
||||
try {
|
||||
const res = await fetch('/api/activity?limit=10');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
let html = '';
|
||||
result.data.forEach(item => {
|
||||
const icon = item.state === 'completed' ? '✓' : '✗';
|
||||
html += `<div class="activity-item">${icon} ${item.relative_path}</div>`;
|
||||
});
|
||||
document.getElementById('activityList').innerHTML = html || '<p>No activity</p>';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function updateLogs() {
|
||||
try {
|
||||
const res = await fetch('/api/logs?lines=50');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
const html = result.data.map(line => `<div>${escapeHtml(line)}</div>`).join('');
|
||||
document.getElementById('logsContainer').innerHTML = html;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user