feat(UI): added docker image and config
This commit is contained in:
@@ -203,6 +203,14 @@ main {
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.schedule-options {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.form-section h4 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.1rem;
|
||||
@@ -770,6 +778,239 @@ footer {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Scheduled Downloads Section */
|
||||
.scheduled-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.scheduled-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scheduled-header h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.queue-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.queue-badge {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.scheduled-container {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-tasks {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.scheduled-list {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.scheduled-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Scheduled Task Card */
|
||||
.task-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.task-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.task-card.disabled {
|
||||
opacity: 0.6;
|
||||
border-left-color: #6c757d;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.task-info h4 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.task-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-meta-item strong {
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-weight: 600;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge.enabled {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-badge.disabled {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.task-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.btn-task {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-toggle:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-toggle.disabled {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.btn-toggle.disabled:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-run {
|
||||
background: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-run:hover {
|
||||
background: #ffca2c;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.btn-history {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-history:hover {
|
||||
background: #138496;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(23, 162, 184, 0.3);
|
||||
}
|
||||
|
||||
/* Task Schedule Info */
|
||||
.task-schedule {
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.task-schedule-item {
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.task-schedule-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-schedule-item strong {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
|
||||
@@ -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