// API endpoints
const API_BASE_URL = '/api/claude';
const ENDPOINTS = {
listJobs: `${API_BASE_URL}/list-jobs`,
manageJob: `${API_BASE_URL}/jobs`,
jobLogs: `${API_BASE_URL}/job-logs`
};
// DOM elements
const elements = {
namespaceSelector: document.getElementById('namespace-selector'),
refreshBtn: document.getElementById('refresh-btn'),
jobList: document.getElementById('job-list'),
jobTable: document.getElementById('job-table'),
jobDetails: document.getElementById('job-details'),
logContent: document.getElementById('log-content'),
logTabs: document.querySelectorAll('.log-tab'),
loading: document.getElementById('loading'),
errorMessage: document.getElementById('error-message')
};
// State
let state = {
jobs: [],
selectedJob: null,
selectedNamespace: 'development',
logs: {
stdout: '',
stderr: '',
currentTab: 'stdout'
}
};
// Initialize the app
function init() {
// Set up event listeners
elements.namespaceSelector.addEventListener('change', handleNamespaceChange);
elements.refreshBtn.addEventListener('click', loadJobs);
elements.logTabs.forEach(tab => {
tab.addEventListener('click', () => {
const logType = tab.getAttribute('data-log-type');
switchLogTab(logType);
});
});
// Load initial jobs
loadJobs();
}
// Load jobs from the API
async function loadJobs() {
showLoading(true);
hideError();
try {
const namespace = elements.namespaceSelector.value;
const response = await fetch(`${ENDPOINTS.listJobs}?namespace=${namespace}`);
if (!response.ok) {
throw new Error(`Failed to load jobs: ${response.statusText}`);
}
const jobs = await response.json();
state.jobs = jobs;
state.selectedNamespace = namespace;
renderJobList();
showLoading(false);
} catch (error) {
console.error('Error loading jobs:', error);
showError(`Failed to load jobs: ${error.message}`);
showLoading(false);
}
}
// Render the job list
function renderJobList() {
elements.jobList.innerHTML = '';
if (state.jobs.length === 0) {
const row = document.createElement('tr');
row.innerHTML = `
No jobs found in the ${state.selectedNamespace} namespace | `;
elements.jobList.appendChild(row);
return;
}
state.jobs.forEach(job => {
const row = document.createElement('tr');
row.setAttribute('data-job-id', job.id);
row.innerHTML = `
${job.id} |
${job.type} |
${job.status} |
|
`;
elements.jobList.appendChild(row);
});
// Add event listeners to buttons
document.querySelectorAll('.btn-view').forEach(btn => {
btn.addEventListener('click', () => viewJob(btn.getAttribute('data-job-id')));
});
document.querySelectorAll('.btn-restart').forEach(btn => {
btn.addEventListener('click', () => restartJob(btn.getAttribute('data-job-id')));
});
document.querySelectorAll('.btn-stop').forEach(btn => {
btn.addEventListener('click', () => stopJob(btn.getAttribute('data-job-id')));
});
}
// View job details
async function viewJob(jobId) {
showLoading(true);
try {
// Get job status
const statusResponse = await fetch(ENDPOINTS.manageJob, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
job_id: jobId,
action: 'status',
namespace: state.selectedNamespace
})
});
if (!statusResponse.ok) {
throw new Error(`Failed to get job status: ${statusResponse.statusText}`);
}
const jobStatus = await statusResponse.json();
state.selectedJob = jobStatus;
// Get job logs
const logsResponse = await fetch(`${ENDPOINTS.jobLogs}/${jobId}?namespace=${state.selectedNamespace}`);
if (logsResponse.ok) {
const logsData = await logsResponse.json();
if (logsData.success) {
state.logs.stdout = logsData.logs.stdout || 'No stdout logs available';
state.logs.stderr = logsData.logs.stderr || 'No stderr logs available';
} else {
state.logs.stdout = 'Logs not available';
state.logs.stderr = 'Logs not available';
}
} else {
state.logs.stdout = 'Failed to load logs';
state.logs.stderr = 'Failed to load logs';
}
renderJobDetails();
renderLogs();
showLoading(false);
// Highlight the selected job in the table
document.querySelectorAll('#job-list tr').forEach(row => {
row.classList.remove('selected');
});
const selectedRow = document.querySelector(`#job-list tr[data-job-id="${jobId}"]`);
if (selectedRow) {
selectedRow.classList.add('selected');
}
} catch (error) {
console.error('Error viewing job:', error);
showError(`Failed to view job: ${error.message}`);
showLoading(false);
}
}
// Restart a job
async function restartJob(jobId) {
if (!confirm(`Are you sure you want to restart job "${jobId}"?`)) {
return;
}
showLoading(true);
try {
const response = await fetch(ENDPOINTS.manageJob, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
job_id: jobId,
action: 'restart',
namespace: state.selectedNamespace
})
});
if (!response.ok) {
throw new Error(`Failed to restart job: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
alert(`Job "${jobId}" has been restarted successfully.`);
loadJobs();
} else {
throw new Error(result.message);
}
showLoading(false);
} catch (error) {
console.error('Error restarting job:', error);
showError(`Failed to restart job: ${error.message}`);
showLoading(false);
}
}
// Stop a job
async function stopJob(jobId) {
const purge = confirm(`Do you want to purge job "${jobId}" after stopping?`);
if (!confirm(`Are you sure you want to stop job "${jobId}"?`)) {
return;
}
showLoading(true);
try {
const response = await fetch(ENDPOINTS.manageJob, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
job_id: jobId,
action: 'stop',
namespace: state.selectedNamespace,
purge: purge
})
});
if (!response.ok) {
throw new Error(`Failed to stop job: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
alert(`Job "${jobId}" has been stopped${purge ? ' and purged' : ''} successfully.`);
loadJobs();
} else {
throw new Error(result.message);
}
showLoading(false);
} catch (error) {
console.error('Error stopping job:', error);
showError(`Failed to stop job: ${error.message}`);
showLoading(false);
}
}
// Render job details
function renderJobDetails() {
if (!state.selectedJob) {
elements.jobDetails.innerHTML = 'Select a job to view details
';
return;
}
const job = state.selectedJob;
const details = job.details?.job || {};
const allocation = job.details?.latest_allocation || {};
let detailsHtml = `
${job.job_id}
Status: ${job.status}
`;
if (details.Type) {
detailsHtml += `Type: ${details.Type}
`;
}
if (details.Namespace) {
detailsHtml += `Namespace: ${details.Namespace}
`;
}
if (details.Datacenters) {
detailsHtml += `Datacenters: ${details.Datacenters.join(', ')}
`;
}
if (allocation.ID) {
detailsHtml += `
Latest Allocation
ID: ${allocation.ID}
Status: ${allocation.ClientStatus || 'Unknown'}
`;
if (allocation.ClientDescription) {
detailsHtml += `Description: ${allocation.ClientDescription}
`;
}
}
elements.jobDetails.innerHTML = detailsHtml;
}
// Render logs
function renderLogs() {
elements.logContent.textContent = state.logs[state.logs.currentTab];
}
// Switch log tab
function switchLogTab(logType) {
state.logs.currentTab = logType;
// Update active tab
elements.logTabs.forEach(tab => {
if (tab.getAttribute('data-log-type') === logType) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
renderLogs();
}
// Handle namespace change
function handleNamespaceChange() {
loadJobs();
}
// Show/hide loading indicator
function showLoading(show) {
elements.loading.style.display = show ? 'block' : 'none';
elements.jobTable.style.display = show ? 'none' : 'table';
}
// Show error message
function showError(message) {
elements.errorMessage.textContent = message;
elements.errorMessage.style.display = 'block';
}
// Hide error message
function hideError() {
elements.errorMessage.style.display = 'none';
}
// Initialize the app when the DOM is loaded
document.addEventListener('DOMContentLoaded', init);