feat(UI): added docker image and config
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user