Files
FitTrack2/FitnessSync/backend/templates/index.html
2026-01-14 05:39:16 -08:00

856 lines
35 KiB
HTML

{% extends "base.html" %}
{% block content %}
<!-- Job Status Banner -->
<!-- Scheduled Jobs Row -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-calendar-check"></i> Scheduled Jobs</h5>
<div>
<button class="btn btn-sm btn-warning me-2" onclick="resetJobsToDefaults()"><i
class="bi bi-arrow-counterclockwise"></i> Reset Defaults</button>
<button class="btn btn-sm btn-success me-2" onclick="createModal.show()"><i
class="bi bi-plus-lg"></i> Add Job</button>
<button class="btn btn-sm btn-light" onclick="loadJobs()"><i class="bi bi-arrow-clockwise"></i>
Refresh</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="scheduler-table">
<thead>
<tr>
<th>Name</th>
<th>Job Type</th>
<th>Status</th>
<th>Interval</th>
<th>Last Run</th>
<th>Next Run</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="7" class="text-center text-muted">Loading schedules...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Job Queue Row -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card border-info">
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-list-task"></i> Active Job Queue</h5>
<span class="badge bg-light text-dark" id="queue-count">0</span>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover mb-0" id="job-queue-table">
<thead>
<tr>
<th>Operation</th>
<th>Job ID</th>
<th>Status</th>
<th>Progress</th>
<th>Message</th>
<th>Started</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" class="text-center text-muted">No active jobs.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Jobs History -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-clock-history"></i> Recent Jobs History</h5>
<div>
<span class="me-2" id="history-page-info">Page 1</span>
<button class="btn btn-sm btn-outline-secondary" onclick="changeHistoryPage(-1)" id="hist-prev-btn"
disabled><i class="bi bi-chevron-left"></i></button>
<button class="btn btn-sm btn-outline-secondary" onclick="changeHistoryPage(1)" id="hist-next-btn"
disabled><i class="bi bi-chevron-right"></i></button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-hover" id="job-history-table">
<thead>
<tr>
<th>Operation</th>
<th>Status</th>
<th>Duration</th>
<th>Completed At</th>
<th>Message</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" class="text-center text-muted">No recent jobs.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Job Details Modal -->
<div class="modal fade" id="jobDetailsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Job Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<dl class="row">
<dt class="col-sm-4">Job ID:</dt>
<dd class="col-sm-8" id="modal-job-id"></dd>
<dt class="col-sm-4">Operation:</dt>
<dd class="col-sm-8" id="modal-job-op"></dd>
<dt class="col-sm-4">Status:</dt>
<dd class="col-sm-8" id="modal-job-status"></dd>
<dt class="col-sm-4">Result:</dt>
<dd class="col-sm-8">
<pre id="modal-job-result" class="bg-light p-2 text-wrap"
style="max-height: 200px; overflow-y: auto;"></pre>
</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Edit Job Modal -->
<div class="modal fade" id="editJobModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Schedule</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="edit-job-form">
<input type="hidden" id="edit-job-id">
<div class="mb-3">
<label class="form-label">Job Name</label>
<input type="text" class="form-control" id="edit-job-name" readonly>
</div>
<div class="mb-3">
<label class="form-label">Interval (Minutes)</label>
<input type="number" class="form-control" id="edit-job-interval" min="0" required>
<div class="form-text">How often this job should run. Set to 0 for Manual Only (Adhoc).</div>
</div>
<div class="mb-3">
<label class="form-label">Parameters (JSON)</label>
<textarea class="form-control text-monospace" id="edit-job-params" rows="3"></textarea>
</div>
<div class="mb-3 form-check form-switch">
<input class="form-check-input" type="checkbox" id="edit-job-enabled">
<label class="form-check-label" for="edit-job-enabled">Enabled</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger"
onclick="deleteJob(document.getElementById('edit-job-id').value)">Delete Job</button>
<button type="button" class="btn btn-primary" onclick="saveJob()">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Create Job Modal -->
<div class="modal fade" id="createJobModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create New Schedule</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="create-job-form">
<div class="mb-3">
<label class="form-label">Job Type</label>
<select class="form-select" id="create-job-type" onchange="updateParamsHelp()">
<option value="" disabled selected>Loading...</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" id="create-job-name" required
placeholder="e.g. Daily Sync">
</div>
<div class="mb-3">
<label class="form-label">Interval (Minutes)</label>
<input type="number" class="form-control" id="create-job-interval" min="0" value="60" required>
<div class="form-text">Set to 0 for Manual Only.</div>
</div>
<div class="mb-3">
<label class="form-label">Parameters (JSON)</label>
<div id="params-helper" class="form-text mb-2"></div>
<textarea class="form-control text-monospace" id="create-job-params"
rows="3">{"days_back": 30}</textarea>
</div>
<div class="mb-3 form-check form-switch">
<input class="form-check-input" type="checkbox" id="create-job-enabled" checked>
<label class="form-check-label" for="create-job-enabled">Enabled immediately</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" onclick="createJob()">Create Schedule</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Recent Sync Logs</h3>
<div class="table-responsive">
<table class="table table-striped" id="sync-logs-table">
<thead>
<tr>
<th>Operation</th>
<th>Status</th>
<th>Start Time</th>
<th>End Time</th>
<th>Processed</th>
<th>Failed</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="7">Loading logs...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<h3>Actions</h3>
<div class="card">
<div class="card-body">
<a href="/docs" class="btn btn-outline-secondary" target="_blank">API Documentation</a>
</div>
</div>
</div>
</div>
<script>
let toastInstance = null;
let jobDetailsModal = null;
let editModal = null;
let createModal = null;
// History Pagination State
let historyPage = 1;
const historyLimit = 10;
let historyTotal = 0;
document.addEventListener('DOMContentLoaded', function () {
const toastEl = document.getElementById('appToast');
if (toastEl) {
toastInstance = new bootstrap.Toast(toastEl);
}
const modalEl = document.getElementById('jobDetailsModal');
if (modalEl) {
jobDetailsModal = new bootstrap.Modal(modalEl);
}
editModal = new bootstrap.Modal(document.getElementById('editJobModal'));
createModal = new bootstrap.Modal(document.getElementById('createJobModal'));
loadJobs();
if (typeof updateParamsHelp === 'function') updateParamsHelp(); // Init helper text
loadDashboardData();
populateJobTypes();
// Auto-refresh dashboard data every 3 seconds
setInterval(loadDashboardData, 3000);
// Listen for global job events to toggle buttons
document.addEventListener('sync-job-active', () => toggleSyncButtons(true));
document.addEventListener('sync-job-finished', () => {
toggleSyncButtons(false);
loadDashboardData(); // Refresh data when job finishes
});
const testBtn = document.getElementById('test-queue-btn');
if (testBtn) {
testBtn.addEventListener('click', async () => {
try {
const res = await fetch('/api/status/test-job', { method: 'POST' });
const data = await res.json();
if (res.ok) showToast('Test Job', 'Test job started (5s)', 'success');
else showToast('Error', data.detail, 'error');
} catch (e) { showToast('Error', e.message, 'error'); }
});
}
});
function showToast(title, body, level = 'info') {
const toastEl = document.getElementById('appToast');
if (!toastEl) return;
// Ensure instance
if (!toastInstance) toastInstance = new bootstrap.Toast(toastEl);
const toastTitle = document.getElementById('toast-title');
const toastBody = document.getElementById('toast-body');
const toastHeader = toastEl.querySelector('.toast-header');
if (toastTitle) toastTitle.textContent = title;
if (toastBody) toastBody.textContent = body;
// Reset header color
toastHeader.classList.remove('bg-success', 'bg-danger', 'bg-warning', 'bg-info', 'text-white');
if (level === 'success') {
toastHeader.classList.add('bg-success', 'text-white');
} else if (level === 'error' || level === 'danger') {
toastHeader.classList.add('bg-danger', 'text-white');
} else if (level === 'warning') {
toastHeader.classList.add('bg-warning');
} else {
toastHeader.classList.add('bg-info', 'text-white');
}
toastInstance.show();
}
async function loadDashboardData() {
try {
// Fetch Status
const response = await fetch('/api/status');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
// Fetch History in parallel
try {
const historyRes = await fetch(`/api/jobs/history?page=${historyPage}&limit=${historyLimit}`);
if (historyRes.ok) {
const historyData = await historyRes.json();
updateJobHistoryTable(historyData);
}
} catch (e) { console.error("History fetch error:", e); }
// Safe update for db-stats if element exists (it should)
const dbStats = document.getElementById('db-stats');
if (dbStats) {
dbStats.innerHTML = `<strong>DB Total Activities:</strong> ${data.total_activities} | <strong>Downloaded:</strong> ${data.downloaded_activities}`;
}
const logsBody = document.querySelector('#sync-logs-table tbody');
if (logsBody) {
logsBody.innerHTML = '';
if (data.recent_logs.length === 0) {
logsBody.innerHTML = '<tr><td colspan="7">No recent sync logs.</td></tr>';
} else {
data.recent_logs.forEach(log => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${log.operation}</td>
<td><span class="badge bg-${log.status === 'completed' ? 'success' : (log.status === 'failed' || log.status === 'cancelled' ? 'danger' : 'warning')}">${log.status}</span></td>
<td>${new Date(log.start_time).toLocaleString()}</td>
<td>${log.end_time ? new Date(log.end_time).toLocaleString() : 'N/A'}</td>
<td>${log.records_processed}</td>
<td>${log.records_failed}</td>
<td>${log.message || ''}</td>
`;
logsBody.appendChild(row);
});
}
}
// Update Job Queue
const queueBody = document.querySelector('#job-queue-table tbody');
const queueCount = document.getElementById('queue-count');
if (queueBody && data.active_jobs) {
if (queueCount) queueCount.textContent = data.active_jobs.length;
queueBody.innerHTML = '';
if (data.active_jobs.length === 0) {
queueBody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No active jobs.</td></tr>';
} else {
data.active_jobs.forEach(job => {
const row = document.createElement('tr');
// Highlight running vs undefined
const statusClass = job.status === 'running' ? 'text-primary fw-bold' : (job.cancel_requested ? 'text-danger' : '');
// Action Buttons Logic
let actionsHtml = '';
if (job.status === 'running' || job.status === 'queued') {
actionsHtml += `<button class="btn btn-sm btn-outline-warning me-1" onclick="pauseJob('${job.id}')" title="Pause"><i class="bi bi-pause-fill"></i></button>`;
} else if (job.status === 'paused') {
actionsHtml += `<button class="btn btn-sm btn-outline-success me-1" onclick="resumeJob('${job.id}')" title="Resume"><i class="bi bi-play-fill"></i></button>`;
}
if (!job.cancel_requested && job.status !== 'completed' && job.status !== 'failed') {
actionsHtml += `<button class="btn btn-sm btn-outline-danger" onclick="cancelJob('${job.id}')" title="Cancel"><i class="bi bi-x-circle"></i></button>`;
}
// force kill button: show if running/queued/paused regardless of cancel_requested
// Use a trash icon or skulls
if (['running', 'queued', 'paused'].includes(job.status)) {
actionsHtml += `<button class="btn btn-sm btn-danger ms-1" onclick="forceKillJob('${job.id}')" title="Force Kill (Mark Failed)"><i class="bi bi-trash-fill"></i></button>`;
}
row.innerHTML = `
<td><span class="${statusClass}">${job.operation}</span></td>
<td><small class="text-muted">${job.id.substring(0, 8)}...</small></td>
<td>
<span class="badge bg-${getBadgeClass(job.status)}">
${job.status} ${job.cancel_requested ? '(Cancelling)' : ''}
</span>
</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
style="width: ${job.progress}%" aria-valuenow="${job.progress}" aria-valuemin="0" aria-valuemax="100">
${job.progress}%
</div>
</div>
</td>
<td><small>${job.message || '-'}</small></td>
<td><small>${new Date(job.start_time).toLocaleTimeString()}</small></td>
<td>${actionsHtml}</td>
`;
queueBody.appendChild(row);
});
}
}
} catch (error) {
console.error('Error loading dashboard data:', error);
// showToast('Error', 'Could not load dashboard data.', 'error');
}
}
function updateJobHistoryTable(data) {
const tbody = document.querySelector('#job-history-table tbody');
if (!tbody) return;
tbody.innerHTML = '';
// Handle {total, items} vs direct list (backwards compact)
const jobs = data.items || data;
historyTotal = data.total || jobs.length;
if (!jobs || jobs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No history available.</td></tr>';
return;
}
jobs.forEach(job => {
const row = document.createElement('tr');
// Safe JSON stringify for onclick
const jobStr = encodeURIComponent(JSON.stringify(job));
row.innerHTML = `
<td>${job.operation}</td>
<td><span class="badge bg-${getBadgeClass(job.status)}">${job.status}</span></td>
<td>${job.duration_s ? job.duration_s + 's' : '-'}</td>
<td>${job.completed_at ? new Date(job.completed_at).toLocaleTimeString() : '-'}</td>
<td><small class="text-truncate d-inline-block" style="max-width: 150px;">${job.message || ''}</small></td>
<td>
<button class="btn btn-sm btn-outline-info" onclick="showJobDetails('${jobStr}')">
<i class="bi bi-info-circle"></i> Details
</button>
</td>
`;
tbody.appendChild(row);
});
// Update pagination UI
document.getElementById('history-page-info').textContent = `Page ${historyPage} of ${Math.ceil(historyTotal / historyLimit) || 1}`;
document.getElementById('hist-prev-btn').disabled = historyPage <= 1;
document.getElementById('hist-next-btn').disabled = historyPage * historyLimit >= historyTotal;
}
function changeHistoryPage(delta) {
historyPage += delta;
if (historyPage < 1) historyPage = 1;
loadDashboardData();
}
function getBadgeClass(status) {
if (status === 'completed') return 'success';
if (status === 'failed' || status === 'cancelled') return 'danger';
if (status === 'running') return 'primary';
if (status === 'paused') return 'warning';
return 'secondary';
}
function showJobDetails(encodedJob) {
try {
const job = JSON.parse(decodeURIComponent(encodedJob));
document.getElementById('modal-job-id').textContent = job.id;
document.getElementById('modal-job-op').textContent = job.operation;
document.getElementById('modal-job-status').textContent = job.status;
let resultText = JSON.stringify(job, null, 2);
if (job.result) resultText = JSON.stringify(job.result, null, 2);
document.getElementById('modal-job-result').textContent = resultText;
if (jobDetailsModal) jobDetailsModal.show();
} catch (e) {
console.error("Error showing details", e);
}
}
async function pauseJob(id) {
try {
await fetch(`/api/jobs/${id}/pause`, { method: 'POST' });
loadDashboardData();
} catch (e) { showToast("Error", "Failed to pause job", "error"); }
}
async function resumeJob(id) {
try {
await fetch(`/api/jobs/${id}/resume`, { method: 'POST' });
loadDashboardData();
} catch (e) { showToast("Error", "Failed to resume job", "error"); }
}
async function cancelJob(id) {
if (!confirm("Are you sure you want to cancel this job?")) return;
try {
await fetch(`/api/jobs/${id}/cancel`, { method: 'POST' });
loadDashboardData();
} catch (e) { showToast("Error", "Failed to cancel job", "error"); }
}
async function forceKillJob(id) {
if (!confirm("WARNING: Force Kill should only be used if a job is stuck!\n\nIt will mark the job as failed immediately but may not stop the background process if it is truly frozen.\n\nAre you sure?")) return;
try {
const res = await fetch(`/api/jobs/${id}/force-kill`, { method: 'POST' });
if (!res.ok) throw new Error("Failed to force kill");
showToast("Force Kill", "Job marked as failed.", "warning");
loadDashboardData();
} catch (e) { showToast("Error", "Failed to force kill job", "error"); }
}
function toggleSyncButtons(disabled) {
const ids = [
'sync-activities-btn', 'sync-all-activities-btn',
'scan-health-btn', 'sync-pending-health-btn',
'sync-fitbit-btn', 'sync-all-fitbit-btn'
];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.disabled = disabled;
});
}
async function resetJobsToDefaults() {
if (!confirm("Are you sure? This will delete all current jobs and restore system defaults.")) return;
try {
const res = await fetch('/api/scheduling/jobs/reset-defaults', { method: 'POST' });
const data = await res.json();
if (res.ok) {
showToast('Reset Complete', data.message, 'success');
loadJobs();
} else {
showToast('Error', data.detail || "Failed to reset", 'error');
}
} catch (e) { showToast('Error', e.message, 'error'); }
}
// --- Scheduler Functions ---
function updateParamsHelp() {
const type = document.getElementById('create-job-type').value;
const helper = document.getElementById('params-helper');
const textArea = document.getElementById('create-job-params');
let helpText = "";
let defaultJson = {};
switch (type) {
case 'fitbit_weight_sync':
case 'activity_sync':
case 'metrics_sync':
case 'health_scan':
helpText = "Requires 'days_back' (integer).";
defaultJson = { "days_back": 10 };
break;
case 'health_sync_pending':
case 'garmin_weight_upload':
helpText = "Requires 'limit' (integer).";
defaultJson = { "limit": 50 };
break;
case 'activity_backfill_full':
helpText = "Requires 'days_back' (integer, default 3650).";
defaultJson = { "days_back": 3650 };
break;
}
helper.textContent = helpText;
textArea.value = JSON.stringify(defaultJson, null, 2);
}
async function createJob() {
const type = document.getElementById('create-job-type').value;
const name = document.getElementById('create-job-name').value;
const interval = parseInt(document.getElementById('create-job-interval').value);
const enabled = document.getElementById('create-job-enabled').checked;
const paramsStr = document.getElementById('create-job-params').value;
if (!name) { alert("Name is required"); return; }
if (interval < 0) { alert("Interval must be >= 0"); return; }
let params = {};
try {
params = JSON.parse(paramsStr);
} catch (e) {
alert("Invalid JSON parameters");
return;
}
try {
const response = await fetch('/api/scheduling/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
job_type: type,
name: name,
interval_minutes: interval,
params: params,
enabled: enabled
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || "Failed to create");
}
createModal.hide();
showToast("Job Created!", "success");
loadJobs();
} catch (e) {
alert("Error: " + e.message);
}
}
async function loadJobs() {
try {
const response = await fetch('/api/scheduling/jobs');
if (!response.ok) throw new Error("Failed to fetch jobs");
const jobs = await response.json();
renderTable(jobs);
} catch (e) {
console.error(e);
showToast("Error loading schedules", "error");
// Render error state table?
}
}
function renderTable(jobs) {
const tbody = document.querySelector('#scheduler-table tbody');
if (!tbody) return;
tbody.innerHTML = '';
if (jobs.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No scheduled jobs configured.</td></tr>';
return;
}
jobs.forEach(job => {
const row = document.createElement('tr');
// Format Next Run color
let nextRunClass = '';
if (!job.enabled) nextRunClass = 'text-muted text-decoration-line-through';
row.innerHTML = `
<td><strong>${job.name}</strong></td>
<td><small class="text-muted">${job.job_type}</small></td>
<td>
<span class="badge bg-${job.enabled ? 'success' : 'secondary'}">
${job.enabled ? 'Active' : 'Disabled'}
</span>
</td>
<td>${job.interval_minutes} mins</td>
<td>${job.last_run ? new Date(job.last_run).toLocaleString() : 'Never'}</td>
<td class="${nextRunClass}">${job.next_run ? new Date(job.next_run).toLocaleString() : '-'}</td>
<td>
<button class="btn btn-sm btn-outline-success me-1" onclick="runJob(${job.id})" title="Run Now">
<i class="bi bi-play-fill"></i> Run
</button>
<button class="btn btn-sm btn-outline-primary" onclick="openEditModal(${job.id}, '${job.name}', ${job.interval_minutes}, ${job.enabled}, '${encodeURIComponent(JSON.stringify(JSON.parse(job.params || '{}')))}')">
<i class="bi bi-pencil"></i> Edit
</button>
</td>
`;
tbody.appendChild(row);
});
}
function openEditModal(id, name, interval, enabled, params) {
document.getElementById('edit-job-id').value = id;
document.getElementById('edit-job-name').value = name;
document.getElementById('edit-job-interval').value = interval;
document.getElementById('edit-job-enabled').checked = enabled;
try {
document.getElementById('edit-job-params').value = JSON.stringify(JSON.parse(decodeURIComponent(params)), null, 2);
} catch (e) {
document.getElementById('edit-job-params').value = "{}";
}
editModal.show();
}
async function deleteJob(id) {
if (!confirm("Are you sure you want to delete this scheduled job?")) return;
try {
const response = await fetch(`/api/scheduling/jobs/${id}`, { method: 'DELETE' });
if (!response.ok) throw new Error("Failed to delete");
editModal.hide();
showToast("Job deleted", "warning");
loadJobs();
} catch (e) {
alert("Error: " + e.message);
}
}
async function runJob(id) {
if (!confirm("Run this scheduled job immediately?")) return;
try {
const response = await fetch(`/api/scheduling/jobs/${id}/run`, { method: 'POST' });
if (!response.ok) throw new Error("Failed to trigger job");
showToast("Job Triggered", "The scheduled job has been started.", "success");
loadJobs();
// Start polling or refresh dashboard active queue
loadDashboardData();
} catch (e) {
showToast("Error", e.message, "error");
}
}
async function saveJob() {
const id = document.getElementById('edit-job-id').value;
const interval = parseInt(document.getElementById('edit-job-interval').value);
const enabled = document.getElementById('edit-job-enabled').checked;
const paramsStr = document.getElementById('edit-job-params').value;
if (interval < 0) {
alert("Interval must be at least 0 minutes");
return;
}
let params = {};
try {
params = JSON.parse(paramsStr);
} catch (e) {
alert("Invalid JSON parameters");
return;
}
try {
const response = await fetch(`/api/scheduling/jobs/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
interval_minutes: interval,
enabled: enabled,
params: params
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || "Failed to update");
}
editModal.hide();
showToast("Schedule updated successfully", "success");
loadJobs();
} catch (e) {
alert("Error: " + e.message);
}
}
async function populateJobTypes() {
const select = document.getElementById('create-job-type');
try {
const response = await fetch('/api/scheduling/available-types');
if (!response.ok) throw new Error('Failed to fetch types');
const types = await response.json();
select.innerHTML = '';
types.forEach(type => {
const option = document.createElement('option');
option.value = type;
// Simple label transformation: underscores to spaces, Title Case
option.text = type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
select.appendChild(option);
});
// Trigger help update for first item
if (types.length > 0) {
select.selectedIndex = 0;
updateParamsHelp();
}
} catch (e) {
console.error("Error loading job types", e);
select.innerHTML = '<option disabled>Error loading types</option>';
}
}
</script>
{% endblock %}