414 lines
15 KiB
JavaScript
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;
|
|
}
|