Files
encoderPro/static/js/dashboard.js
2026-01-24 17:43:28 -05:00

414 lines
15 KiB
JavaScript

// 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;
}