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
+241
View File
@@ -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); }
+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