271 lines
8.4 KiB
JavaScript
271 lines
8.4 KiB
JavaScript
const API_BASE = '/api';
|
|
|
|
let currentUser = null;
|
|
|
|
// Check authentication on load
|
|
async function init() {
|
|
await checkAuth();
|
|
setupEventListeners();
|
|
if (currentUser) {
|
|
showMainPage();
|
|
loadJobs();
|
|
loadRunners();
|
|
} else {
|
|
showLoginPage();
|
|
}
|
|
}
|
|
|
|
async function checkAuth() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/me`);
|
|
if (response.ok) {
|
|
currentUser = await response.json();
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function showLoginPage() {
|
|
document.getElementById('login-page').classList.remove('hidden');
|
|
document.getElementById('main-page').classList.add('hidden');
|
|
}
|
|
|
|
function showMainPage() {
|
|
document.getElementById('login-page').classList.add('hidden');
|
|
document.getElementById('main-page').classList.remove('hidden');
|
|
if (currentUser) {
|
|
document.getElementById('user-name').textContent = currentUser.name || currentUser.email;
|
|
}
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Navigation
|
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const page = e.target.dataset.page;
|
|
switchPage(page);
|
|
});
|
|
});
|
|
|
|
// Logout
|
|
document.getElementById('logout-btn').addEventListener('click', async () => {
|
|
await fetch(`${API_BASE}/auth/logout`, { method: 'POST' });
|
|
currentUser = null;
|
|
showLoginPage();
|
|
});
|
|
|
|
// Job form
|
|
document.getElementById('job-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
await submitJob();
|
|
});
|
|
}
|
|
|
|
function switchPage(page) {
|
|
document.querySelectorAll('.content-page').forEach(p => p.classList.add('hidden'));
|
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
|
|
|
document.getElementById(`${page}-page`).classList.remove('hidden');
|
|
document.querySelector(`[data-page="${page}"]`).classList.add('active');
|
|
|
|
if (page === 'jobs') {
|
|
loadJobs();
|
|
} else if (page === 'runners') {
|
|
loadRunners();
|
|
}
|
|
}
|
|
|
|
async function submitJob() {
|
|
const form = document.getElementById('job-form');
|
|
const formData = new FormData(form);
|
|
|
|
const jobData = {
|
|
name: document.getElementById('job-name').value,
|
|
frame_start: parseInt(document.getElementById('frame-start').value),
|
|
frame_end: parseInt(document.getElementById('frame-end').value),
|
|
output_format: document.getElementById('output-format').value,
|
|
};
|
|
|
|
try {
|
|
// Create job
|
|
const jobResponse = await fetch(`${API_BASE}/jobs`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(jobData),
|
|
});
|
|
|
|
if (!jobResponse.ok) {
|
|
throw new Error('Failed to create job');
|
|
}
|
|
|
|
const job = await jobResponse.json();
|
|
|
|
// Upload file
|
|
const fileInput = document.getElementById('blend-file');
|
|
if (fileInput.files.length > 0) {
|
|
const fileFormData = new FormData();
|
|
fileFormData.append('file', fileInput.files[0]);
|
|
|
|
const fileResponse = await fetch(`${API_BASE}/jobs/${job.id}/upload`, {
|
|
method: 'POST',
|
|
body: fileFormData,
|
|
});
|
|
|
|
if (!fileResponse.ok) {
|
|
throw new Error('Failed to upload file');
|
|
}
|
|
}
|
|
|
|
alert('Job submitted successfully!');
|
|
form.reset();
|
|
switchPage('jobs');
|
|
loadJobs();
|
|
} catch (error) {
|
|
alert('Failed to submit job: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function loadJobs() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/jobs`);
|
|
if (!response.ok) throw new Error('Failed to load jobs');
|
|
|
|
const jobs = await response.json();
|
|
displayJobs(jobs);
|
|
} catch (error) {
|
|
console.error('Failed to load jobs:', error);
|
|
}
|
|
}
|
|
|
|
function displayJobs(jobs) {
|
|
const container = document.getElementById('jobs-list');
|
|
if (jobs.length === 0) {
|
|
container.innerHTML = '<p>No jobs yet. Submit a job to get started!</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = jobs.map(job => `
|
|
<div class="job-card">
|
|
<h3>${escapeHtml(job.name)}</h3>
|
|
<div class="job-meta">
|
|
<span>Frames: ${job.frame_start}-${job.frame_end}</span>
|
|
<span>Format: ${job.output_format}</span>
|
|
<span>Created: ${new Date(job.created_at).toLocaleString()}</span>
|
|
</div>
|
|
<div class="job-status ${job.status}">${job.status}</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${job.progress}%"></div>
|
|
</div>
|
|
<div class="job-actions">
|
|
<button onclick="viewJob(${job.id})" class="btn btn-primary">View Details</button>
|
|
${job.status === 'pending' || job.status === 'running' ?
|
|
`<button onclick="cancelJob(${job.id})" class="btn btn-secondary">Cancel</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async function viewJob(jobId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/jobs/${jobId}`);
|
|
if (!response.ok) throw new Error('Failed to load job');
|
|
|
|
const job = await response.json();
|
|
|
|
// Load files
|
|
const filesResponse = await fetch(`${API_BASE}/jobs/${jobId}/files`);
|
|
const files = filesResponse.ok ? await filesResponse.json() : [];
|
|
|
|
const outputFiles = files.filter(f => f.file_type === 'output');
|
|
if (outputFiles.length > 0) {
|
|
let message = 'Output files:\n';
|
|
outputFiles.forEach(file => {
|
|
message += `- ${file.file_name}\n`;
|
|
});
|
|
message += '\nWould you like to download them?';
|
|
if (confirm(message)) {
|
|
outputFiles.forEach(file => {
|
|
window.open(`${API_BASE}/jobs/${jobId}/files/${file.id}/download`, '_blank');
|
|
});
|
|
}
|
|
} else {
|
|
alert(`Job: ${job.name}\nStatus: ${job.status}\nProgress: ${job.progress.toFixed(1)}%`);
|
|
}
|
|
} catch (error) {
|
|
alert('Failed to load job details: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function cancelJob(jobId) {
|
|
if (!confirm('Are you sure you want to cancel this job?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/jobs/${jobId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!response.ok) throw new Error('Failed to cancel job');
|
|
loadJobs();
|
|
} catch (error) {
|
|
alert('Failed to cancel job: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function loadRunners() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/runners`);
|
|
if (!response.ok) throw new Error('Failed to load runners');
|
|
|
|
const runners = await response.json();
|
|
displayRunners(runners);
|
|
} catch (error) {
|
|
console.error('Failed to load runners:', error);
|
|
}
|
|
}
|
|
|
|
function displayRunners(runners) {
|
|
const container = document.getElementById('runners-list');
|
|
if (runners.length === 0) {
|
|
container.innerHTML = '<p>No runners connected.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = runners.map(runner => {
|
|
const lastHeartbeat = new Date(runner.last_heartbeat);
|
|
const isOnline = (Date.now() - lastHeartbeat.getTime()) < 60000; // 1 minute
|
|
|
|
return `
|
|
<div class="runner-card">
|
|
<h3>${escapeHtml(runner.name)}</h3>
|
|
<div class="runner-info">
|
|
<span>Hostname: ${escapeHtml(runner.hostname)}</span>
|
|
<span>IP: ${escapeHtml(runner.ip_address)}</span>
|
|
<span>Last heartbeat: ${lastHeartbeat.toLocaleString()}</span>
|
|
</div>
|
|
<div class="runner-status ${isOnline ? 'online' : 'offline'}">
|
|
${isOnline ? 'Online' : 'Offline'}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Auto-refresh jobs every 5 seconds
|
|
setInterval(() => {
|
|
if (currentUser && document.getElementById('jobs-page').classList.contains('hidden') === false) {
|
|
loadJobs();
|
|
}
|
|
}, 5000);
|
|
|
|
// Initialize on load
|
|
init();
|
|
|