feat(UI): added docker image and config

This commit is contained in:
2025-10-10 11:15:28 +13:00
parent 9e61d18bf6
commit 9f5a25fcf5
23 changed files with 3287 additions and 18 deletions
+309
View File
@@ -7,6 +7,7 @@ class BDFRApp {
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.downloads = new Map();
this.scheduledTasks = new Map();
this.authState = null;
this.authenticated = false;
@@ -19,6 +20,10 @@ class BDFRApp {
// Check for stored auth state first
this.authState = this.getStoredAuthState();
this.checkAuthentication();
// Load scheduled tasks
this.loadScheduledTasks();
this.startQueuePolling();
}
initializeElements() {
@@ -32,6 +37,13 @@ class BDFRApp {
this.downloadsList = document.getElementById('downloadsList');
this.downloadsItems = document.getElementById('downloadsItems');
// Scheduled tasks containers
this.scheduledContainer = document.getElementById('scheduledContainer');
this.scheduledList = document.getElementById('scheduledList');
this.scheduledItems = document.getElementById('scheduledItems');
this.queueStatus = document.getElementById('queueStatus');
this.queueCount = document.getElementById('queueCount');
// Status elements
this.wsStatus = document.getElementById('wsStatus');
this.bdfrStatus = document.getElementById('bdfrStatus');
@@ -44,6 +56,12 @@ class BDFRApp {
this.logoutBtn = document.getElementById('logoutBtn');
this.authStateInput = document.getElementById('authState');
this.userAuthStateInput = document.getElementById('userAuthState');
// Scheduled task form elements
this.runDailyCheckbox = document.getElementById('runDaily');
this.scheduleOptions = document.getElementById('scheduleOptions');
this.taskNameInput = document.getElementById('taskName');
this.runTimeInput = document.getElementById('runTime');
}
bindEvents() {
@@ -57,6 +75,21 @@ class BDFRApp {
radio.addEventListener('change', (e) => this.updateSourceTypeUI(e.target.value));
});
}
// Run Daily checkbox toggle
if (this.runDailyCheckbox) {
this.runDailyCheckbox.addEventListener('change', (e) => {
this.scheduleOptions.style.display = e.target.checked ? 'block' : 'none';
if (e.target.checked && !this.taskNameInput.value) {
// Auto-generate task name
const sourceType = document.querySelector('input[name="source_type"]:checked').value;
const sourceName = document.getElementById('sourceName').value.trim();
if (sourceName) {
this.taskNameInput.value = `Daily ${sourceName} ${sourceType}`;
}
}
});
}
if (this.subredditForm) {
this.subredditForm.addEventListener('submit', (e) => this.handleSubredditSubmit(e));
@@ -292,6 +325,7 @@ class BDFRApp {
const sort = formData.get('sort');
const noDupes = document.getElementById('noDupes').checked;
const simpleCheck = document.getElementById('simpleCheck').checked;
const runDaily = this.runDailyCheckbox.checked;
// Validate source name
if (sourceType === 'subreddit' && !this.validateSubreddit(sourceName)) {
@@ -303,6 +337,12 @@ class BDFRApp {
return;
}
// If Run Daily is checked, create scheduled task instead
if (runDaily) {
await this.createScheduledTask(e);
return;
}
// Build confirmation message
const modeLabels = {
'download': 'Download (media files)',
@@ -994,6 +1034,275 @@ class BDFRApp {
this.showError('Authentication failed');
}
}
// ===== Scheduled Tasks Methods =====
async loadScheduledTasks() {
try {
const response = await fetch('/api/scheduled-tasks');
const tasks = await response.json();
if (response.ok) {
this.scheduledTasks.clear();
tasks.forEach(task => {
this.scheduledTasks.set(task.id, task);
});
this.renderScheduledTasks();
}
} catch (error) {
console.error('Failed to load scheduled tasks:', error);
}
}
renderScheduledTasks() {
if (this.scheduledTasks.size > 0) {
this.scheduledContainer.style.display = 'none';
this.scheduledList.style.display = 'block';
this.scheduledItems.innerHTML = '';
this.scheduledTasks.forEach(task => {
const card = this.createTaskCard(task);
this.scheduledItems.appendChild(card);
});
} else {
this.scheduledContainer.style.display = 'flex';
this.scheduledList.style.display = 'none';
}
}
createTaskCard(task) {
const card = document.createElement('div');
card.className = `task-card ${task.enabled ? '' : 'disabled'}`;
card.id = `task-${task.id}`;
const sourceLabel = task.source_type === 'subreddit' ? `r/${task.source_name}` : `u/${task.source_name}`;
const modeLabel = task.download_mode.charAt(0).toUpperCase() + task.download_mode.slice(1);
const lastRun = task.last_run_at ? new Date(task.last_run_at).toLocaleString() : 'Never';
const nextRun = task.next_run_at ? new Date(task.next_run_at).toLocaleString() : 'Not scheduled';
card.innerHTML = `
<div class="task-header">
<div class="task-info">
<h4>${task.name}</h4>
<div class="task-meta">
<div class="task-meta-item">
<strong>Source:</strong> ${sourceLabel}
</div>
<div class="task-meta-item">
<strong>Mode:</strong> ${modeLabel}
</div>
<div class="task-meta-item">
<strong>Schedule:</strong> Daily at ${task.run_time}
</div>
</div>
</div>
<div class="task-status">
<span class="status-badge ${task.enabled ? 'enabled' : 'disabled'}">
${task.enabled ? '✓ Enabled' : '✗ Disabled'}
</span>
</div>
</div>
<div class="task-schedule">
<div class="task-schedule-item"><strong>Last Run:</strong> ${lastRun}</div>
<div class="task-schedule-item"><strong>Next Run:</strong> ${nextRun}</div>
</div>
<div class="task-controls">
<button class="btn-task btn-toggle ${task.enabled ? '' : 'disabled'}" data-task-id="${task.id}" data-action="toggle">
${task.enabled ? 'Disable' : 'Enable'}
</button>
<button class="btn-task btn-run" data-task-id="${task.id}" data-action="run">
Run Now
</button>
<button class="btn-task btn-delete" data-task-id="${task.id}" data-action="delete">
Delete
</button>
</div>
`;
// Add event listeners to buttons
const toggleBtn = card.querySelector('[data-action="toggle"]');
const runBtn = card.querySelector('[data-action="run"]');
const deleteBtn = card.querySelector('[data-action="delete"]');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggleTask(task.id));
}
if (runBtn) {
runBtn.addEventListener('click', () => this.runTaskNow(task.id));
}
if (deleteBtn) {
deleteBtn.addEventListener('click', () => this.deleteTask(task.id));
}
return card;
}
async createScheduledTask(e) {
const formData = new FormData(e.target);
const taskName = this.taskNameInput.value.trim();
const runTime = this.runTimeInput.value;
if (!taskName) {
this.showError('Please enter a task name');
return;
}
if (!runTime) {
this.showError('Please select a run time');
return;
}
const downloadMode = formData.get('download_mode');
const sourceType = formData.get('source_type');
const sourceName = formData.get('source_name').trim();
const limit = formData.get('limit');
const sort = formData.get('sort');
// Get browser timezone
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const taskData = {
name: taskName,
source_type: sourceType,
source_name: sourceName,
download_mode: downloadMode,
limit: parseInt(limit),
sort: sort,
run_time: runTime,
timezone: timezone,
enabled: true
};
try {
this.showLoading(e.target.querySelector('button'));
const response = await fetch('/api/scheduled-tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(taskData)
});
const result = await response.json();
if (response.ok) {
this.showSuccess(`Scheduled task created: ${taskName}`);
e.target.reset();
this.runDailyCheckbox.checked = false;
this.scheduleOptions.style.display = 'none';
await this.loadScheduledTasks();
} else {
this.showError(result.detail || 'Failed to create scheduled task');
}
} catch (error) {
console.error('Error:', error);
this.showError('Network error occurred');
} finally {
this.hideLoading(e.target.querySelector('button'));
}
}
async toggleTask(taskId) {
try {
const response = await fetch(`/api/scheduled-tasks/${taskId}/toggle`, {
method: 'POST'
});
const result = await response.json();
if (response.ok) {
this.showSuccess(`Task ${result.enabled ? 'enabled' : 'disabled'}`);
await this.loadScheduledTasks();
} else {
this.showError(result.detail || 'Failed to toggle task');
}
} catch (error) {
console.error('Error:', error);
this.showError('Failed to toggle task');
}
}
async runTaskNow(taskId) {
if (!confirm('Run this scheduled task now?')) {
return;
}
try {
const response = await fetch(`/api/scheduled-tasks/${taskId}/run-now`, {
method: 'POST'
});
const result = await response.json();
if (response.ok) {
this.showSuccess('Task added to queue');
await this.loadScheduledTasks();
} else {
this.showError(result.detail || 'Failed to queue task');
}
} catch (error) {
console.error('Error:', error);
this.showError('Failed to queue task');
}
}
async deleteTask(taskId) {
if (!confirm('Delete this scheduled task? This cannot be undone.')) {
return;
}
try {
const response = await fetch(`/api/scheduled-tasks/${taskId}`, {
method: 'DELETE'
});
if (response.ok) {
this.showSuccess('Task deleted');
await this.loadScheduledTasks();
} else {
const result = await response.json();
this.showError(result.detail || 'Failed to delete task');
}
} catch (error) {
console.error('Error:', error);
this.showError('Failed to delete task');
}
}
async updateQueueStatus() {
try {
const response = await fetch('/api/scheduled-tasks/queue/status');
const status = await response.json();
if (response.ok) {
const queueSize = status.queue_size || 0;
if (queueSize > 0 || status.current_task) {
this.queueStatus.style.display = 'block';
this.queueCount.textContent = queueSize;
} else {
this.queueStatus.style.display = 'none';
}
}
} catch (error) {
console.error('Failed to update queue status:', error);
}
}
startQueuePolling() {
// Update queue status every 10 seconds
setInterval(() => {
this.updateQueueStatus();
}, 10000);
// Initial update
this.updateQueueStatus();
}
}
// Initialize the application when DOM is loaded