initial commit
This commit is contained in:
270
web/app.js
Normal file
270
web/app.js
Normal file
@@ -0,0 +1,270 @@
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user