Files
decypharr/pkg/web/templates/repair.html
2025-06-26 07:31:12 +01:00

863 lines
37 KiB
HTML

{{ define "repair" }}
<div class="container mt-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0"><i class="bi bi-tools me-2"></i>Repair Media</h4>
</div>
<div class="card-body">
<form id="repairForm">
<div class="mb-3">
<label for="arrSelect" class="form-label">Select Arr Instance</label>
<select class="form-select" id="arrSelect">
<option value="">Select an Arr instance</option>
</select>
</div>
<div class="mb-3">
<label for="mediaIds" class="form-label">Media IDs</label>
<input type="text" class="form-control" id="mediaIds"
placeholder="Enter IDs (comma-separated)">
<small class="text-muted">Enter TV DB ids for Sonarr, TM DB ids for Radarr</small>
</div>
<div class="mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isAsync" checked>
<label class="form-check-label" for="isAsync">
Run in background
</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="autoProcess">
<label class="form-check-label" for="autoProcess">
Auto Process(this will delete and re-search broken media)
</label>
</div>
</div>
<button type="submit" class="btn btn-primary" id="submitRepair">
<i class="bi bi-wrench me-2"></i>Start Repair
</button>
</form>
</div>
</div>
<!-- Jobs Table Section -->
<div class="card mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-list-task me-2"></i>Repair Jobs</h4>
<div>
<button id="deleteSelectedJobs" class="btn btn-sm btn-danger me-2" disabled>
<i class="bi bi-trash me-1"></i>Delete Selected
</button>
<button id="refreshJobs" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="jobsTable">
<thead>
<tr>
<th>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllJobs">
</div>
</th>
<th>ID</th>
<th>Arr Instances</th>
<th>Started</th>
<th>Status</th>
<th>Broken Items</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="jobsTableBody">
<!-- Jobs will be loaded here -->
</tbody>
</table>
</div>
<!-- Pagination -->
<nav aria-label="Jobs pagination" class="mt-3">
<ul class="pagination justify-content-center" id="jobsPagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
<div id="noJobsMessage" class="text-center py-3 d-none">
<p class="text-muted">No repair jobs found</p>
</div>
</div>
</div>
<!-- Job Details Modal -->
<div class="modal fade" id="jobDetailsModal" tabindex="-1" aria-labelledby="jobDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobDetailsModalLabel">Job Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Job Info -->
<div class="row mb-4">
<div class="col-md-6">
<p><strong>Job ID:</strong> <span id="modalJobId"></span></p>
<p><strong>Status:</strong> <span id="modalJobStatus"></span></p>
<p><strong>Started:</strong> <span id="modalJobStarted"></span></p>
<p><strong>Completed:</strong> <span id="modalJobCompleted"></span></p>
</div>
<div class="col-md-6">
<p><strong>Arrs:</strong> <span id="modalJobArrs"></span></p>
<p><strong>Media IDs:</strong> <span id="modalJobMediaIds"></span></p>
<p><strong>Auto Process:</strong> <span id="modalJobAutoProcess"></span></p>
</div>
</div>
<div id="errorContainer" class="alert alert-danger mb-3 d-none">
<strong>Error:</strong> <span id="modalJobError"></span>
</div>
<!-- Broken Items Section -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">
Broken Items
<span class="badge bg-secondary" id="totalItemsCount">0</span>
</h6>
</div>
<!-- Filters and Search -->
<div class="row mb-3">
<div class="col-md-4">
<input type="text" class="form-control form-control-sm"
id="itemSearchInput"
placeholder="Search by path...">
</div>
<div class="col-md-3">
<select class="form-select form-select-sm" id="arrFilterSelect">
<option value="">All Arrs</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select form-select-sm" id="pathFilterSelect">
<option value="">All Types</option>
<option value="movie">Movies</option>
<option value="tv">TV Shows</option>
<option value="other">Other</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-outline-secondary w-100" id="clearFiltersBtn">
<i class="bi bi-x-circle me-1"></i>Clear
</button>
</div>
</div>
<!-- Items Table -->
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-sm table-striped table-hover">
<thead class="sticky-top">
<tr>
<th>Arr</th>
<th>Path</th>
<th style="width: 100px;">Type</th>
<th style="width: 80px;">Size</th>
</tr>
</thead>
<tbody id="brokenItemsTableBody">
<!-- Broken items will be loaded here -->
</tbody>
</table>
</div>
<!-- Items Pagination -->
<nav aria-label="Items pagination" class="mt-2">
<ul class="pagination pagination-sm justify-content-center" id="itemsPagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
<div id="noBrokenItemsMessage" class="text-center py-3 d-none">
<p class="text-muted">No broken items found</p>
</div>
<div id="noFilteredItemsMessage" class="text-center py-3 d-none">
<p class="text-muted">No items match the current filters</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="me-auto">
<small class="text-muted" id="modalFooterStats"></small>
</div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="processJobBtn">Process All Items</button>
<button type="button" class="btn btn-warning d-none" id="stopJobBtn">
<i class="bi bi-stop-fill me-1"></i>Stop Job
</button>
</div>
</div>
</div>
</div>
</div>
<style>
.sticky-top {
position: sticky;
top: 0;
z-index: 10;
}
.table-hover tbody tr:hover {
background-color: var(--bs-gray-100);
}
[data-bs-theme="dark"] .table-hover tbody tr:hover {
background-color: var(--bs-gray-800);
}
.item-row.selected {
background-color: var(--bs-primary-bg-subtle) !important;
}
.badge {
font-size: 0.75em;
}
#brokenItemsTableBody tr {
cursor: pointer;
}
.form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Load Arr instances
fetcher('/api/arrs')
.then(response => response.json())
.then(arrs => {
const select = document.getElementById('arrSelect');
arrs.forEach(arr => {
const option = document.createElement('option');
option.value = arr.name;
option.textContent = arr.name;
select.appendChild(option);
});
});
// Handle form submission
document.getElementById('repairForm').addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('submitRepair');
const originalText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Repairing...';
let mediaIds = document.getElementById('mediaIds').value.split(',').map(id => id.trim());
let arr = document.getElementById('arrSelect').value;
try {
const response = await fetcher('/api/repair', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
arr: arr,
mediaIds: mediaIds,
async: document.getElementById('isAsync').checked,
autoProcess: document.getElementById('autoProcess').checked,
})
});
if (!response.ok) throw new Error(await response.text());
createToast('Repair process initiated successfully!');
await loadJobs(1); // Refresh jobs after submission
} catch (error) {
createToast(`Error starting repair: ${error.message}`, 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
});
// Jobs table pagination variables
let currentPage = 1;
const itemsPerPage = 10;
let allJobs = [];
// Modal state variables
let currentJob = null;
let allBrokenItems = [];
let filteredItems = [];
let selectedItems = new Set();
let currentItemsPage = 1;
const itemsPerModalPage = 20;
// Load jobs function
async function loadJobs(page) {
try {
const response = await fetcher('/api/repair/jobs');
if (!response.ok) throw new Error('Failed to fetch jobs');
allJobs = await response.json();
renderJobsTable(page);
} catch (error) {
console.error('Error loading jobs:', error);
createToast(`Error loading jobs: ${error.message}`, 'error');
}
}
// Return status text and class based on job status
function getStatus(status) {
switch (status) {
case 'started':
return {text: 'In Progress', class: 'text-primary'};
case 'failed':
return {text: 'Failed', class: 'text-danger'};
case 'completed':
return {text: 'Completed', class: 'text-success'};
case 'pending':
return {text: 'Pending', class: 'text-warning'};
case 'cancelled':
return {text: 'Cancelled', class: 'text-secondary'};
case 'processing':
return {text: 'Processing', class: 'text-info'};
default:
return {text: status.charAt(0).toUpperCase() + status.slice(1), class: 'text-secondary'};
}
}
// Render jobs table with pagination (keeping existing implementation)
function renderJobsTable(page) {
const tableBody = document.getElementById('jobsTableBody');
const paginationElement = document.getElementById('jobsPagination');
const noJobsMessage = document.getElementById('noJobsMessage');
const deleteSelectedBtn = document.getElementById('deleteSelectedJobs');
// Clear previous content
tableBody.innerHTML = '';
paginationElement.innerHTML = '';
document.getElementById('selectAllJobs').checked = false;
deleteSelectedBtn.disabled = true;
if (allJobs.length === 0) {
noJobsMessage.classList.remove('d-none');
return;
}
noJobsMessage.classList.add('d-none');
// Calculate pagination
const totalPages = Math.ceil(allJobs.length / itemsPerPage);
const startIndex = (page - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, allJobs.length);
// Display jobs for current page
for (let i = startIndex; i < endIndex; i++) {
const job = allJobs[i];
const row = document.createElement('tr');
// Format date
const startedDate = new Date(job.created_at);
const formattedDate = startedDate.toLocaleString();
// Determine status
let status = getStatus(job.status);
let canDelete = job.status !== "started";
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
row.innerHTML = `
<td>
<div class="form-check">
<input class="form-check-input job-checkbox" type="checkbox" value="${job.id}"
${canDelete ? '' : 'disabled'} data-can-delete="${canDelete}">
</div>
</td>
<td><a href="#" class="text-link view-job" data-id="${job.id}"><small>${job.id.substring(0, 8)}</small></a></td>
<td>${job.arrs.join(', ')}</td>
<td><small>${formattedDate}</small></td>
<td><span class="${status.class}">${status.text}</span></td>
<td>${totalItems}</td>
<td>
${job.status === "pending" ?
`<button class="btn btn-sm btn-primary process-job" data-id="${job.id}">
<i class="bi bi-play-fill"></i> Process
</button>` :
`<button class="btn btn-sm btn-primary" disabled>
<i class="bi bi-eye"></i> Process
</button>`
}
${(job.status === "started" || job.status === "processing") ?
`<button class="btn btn-sm btn-warning stop-job" data-id="${job.id}">
<i class="bi bi-stop-fill"></i> Stop
</button>` :
''
}
${canDelete ?
`<button class="btn btn-sm btn-danger delete-job" data-id="${job.id}">
<i class="bi bi-trash"></i>
</button>` :
`<button class="btn btn-sm btn-danger" disabled>
<i class="bi bi-trash"></i>
</button>`
}
</td>
`;
tableBody.appendChild(row);
}
// Create pagination (keeping existing implementation)
if (totalPages > 1) {
const prevLi = document.createElement('li');
prevLi.className = `page-item ${page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" aria-label="Previous" ${page !== 1 ? `data-page="${page - 1}"` : ''}>
<span aria-hidden="true">&laquo;</span>
</a>`;
paginationElement.appendChild(prevLi);
for (let i = 1; i <= totalPages; i++) {
const pageLi = document.createElement('li');
pageLi.className = `page-item ${i === page ? 'active' : ''}`;
pageLi.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
paginationElement.appendChild(pageLi);
}
const nextLi = document.createElement('li');
nextLi.className = `page-item ${page === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" aria-label="Next" ${page !== totalPages ? `data-page="${page + 1}"` : ''}>
<span aria-hidden="true">&raquo;</span>
</a>`;
paginationElement.appendChild(nextLi);
}
// Add event listeners (keeping existing implementation)
document.querySelectorAll('#jobsPagination a[data-page]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
currentPage = parseInt(e.currentTarget.dataset.page);
renderJobsTable(currentPage);
});
});
document.querySelectorAll('.job-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateDeleteButtonState);
});
document.querySelectorAll('.delete-job').forEach(button => {
button.addEventListener('click', (e) => {
const jobId = e.currentTarget.dataset.id;
deleteJob(jobId);
});
});
document.querySelectorAll('.process-job').forEach(button => {
button.addEventListener('click', (e) => {
const jobId = e.currentTarget.dataset.id;
processJob(jobId);
});
});
document.querySelectorAll('.view-job').forEach(button => {
button.addEventListener('click', (e) => {
const jobId = e.currentTarget.dataset.id;
viewJobDetails(jobId);
});
});
document.querySelectorAll('.stop-job').forEach(button => {
button.addEventListener('click', (e) => {
const jobId = e.currentTarget.dataset.id;
stopJob(jobId);
});
});
}
// Helper functions to determine file type and format size
function getFileType(path) {
const movieExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'];
const tvIndicators = ['/TV/', '/Television/', '/Series/', '/Shows/'];
const pathLower = path.toLowerCase();
if (tvIndicators.some(indicator => pathLower.includes(indicator.toLowerCase()))) {
return 'tv';
}
if (movieExtensions.some(ext => pathLower.endsWith(ext))) {
return pathLower.includes('/movies/') || pathLower.includes('/film') ? 'movie' : 'tv';
}
return 'other';
}
function formatFileSize(bytes) {
if (!bytes || bytes === 0) return 'Unknown';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
// modal functions
function processItemsData(brokenItems) {
const items = [];
for (const [arrName, itemsArray] of Object.entries(brokenItems)) {
if (itemsArray && itemsArray.length > 0) {
itemsArray.forEach(item => {
items.push({
id: item.fileId,
arr: arrName,
path: item.path,
size: item.size || 0,
type: getFileType(item.path),
selected: false
});
});
}
}
return items;
}
function applyFilters() {
const searchTerm = document.getElementById('itemSearchInput').value.toLowerCase();
const arrFilter = document.getElementById('arrFilterSelect').value;
const pathFilter = document.getElementById('pathFilterSelect').value;
filteredItems = allBrokenItems.filter(item => {
const matchesSearch = !searchTerm || item.path.toLowerCase().includes(searchTerm);
const matchesArr = !arrFilter || item.arr === arrFilter;
const matchesPath = !pathFilter || item.type === pathFilter;
return matchesSearch && matchesArr && matchesPath;
});
currentItemsPage = 1;
renderBrokenItemsTable();
updateItemsStats();
}
function renderBrokenItemsTable() {
const tableBody = document.getElementById('brokenItemsTableBody');
const paginationElement = document.getElementById('itemsPagination');
const noItemsMessage = document.getElementById('noBrokenItemsMessage');
const noFilteredMessage = document.getElementById('noFilteredItemsMessage');
tableBody.innerHTML = '';
paginationElement.innerHTML = '';
if (allBrokenItems.length === 0) {
noItemsMessage.classList.remove('d-none');
noFilteredMessage.classList.add('d-none');
return;
}
if (filteredItems.length === 0) {
noItemsMessage.classList.add('d-none');
noFilteredMessage.classList.remove('d-none');
return;
}
noItemsMessage.classList.add('d-none');
noFilteredMessage.classList.add('d-none');
// Calculate pagination
const totalPages = Math.ceil(filteredItems.length / itemsPerModalPage);
const startIndex = (currentItemsPage - 1) * itemsPerModalPage;
const endIndex = Math.min(startIndex + itemsPerModalPage, filteredItems.length);
// Display items for current page
for (let i = startIndex; i < endIndex; i++) {
const item = filteredItems[i];
const row = document.createElement('tr');
row.className = `item-row ${selectedItems.has(item.id) ? 'selected' : ''}`;
row.dataset.itemId = item.id;
row.innerHTML = `
<td><span class="badge bg-info">${item.arr}</span></td>
<td><small class="text-muted" title="${item.path}">${item.path}</small></td>
<td><span class="badge ${item.type === 'movie' ? 'bg-primary' : item.type === 'tv' ? 'bg-success' : 'bg-secondary'}">${item.type}</span></td>
<td><small>${formatFileSize(item.size)}</small></td>
`;
tableBody.appendChild(row);
}
// Create pagination
if (totalPages > 1) {
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentItemsPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" aria-label="Previous" ${currentItemsPage !== 1 ? `data-items-page="${currentItemsPage - 1}"` : ''}>
<span aria-hidden="true">&laquo;</span>
</a>`;
paginationElement.appendChild(prevLi);
for (let i = 1; i <= totalPages; i++) {
const pageLi = document.createElement('li');
pageLi.className = `page-item ${i === currentItemsPage ? 'active' : ''}`;
pageLi.innerHTML = `<a class="page-link" href="#" data-items-page="${i}">${i}</a>`;
paginationElement.appendChild(pageLi);
}
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentItemsPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" aria-label="Next" ${currentItemsPage !== totalPages ? `data-items-page="${currentItemsPage + 1}"` : ''}>
<span aria-hidden="true">&raquo;</span>
</a>`;
paginationElement.appendChild(nextLi);
}
// Add pagination event listeners
document.querySelectorAll('#itemsPagination a[data-items-page]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
currentItemsPage = parseInt(e.currentTarget.dataset.itemsPage);
renderBrokenItemsTable();
});
});
}
function updateItemsStats() {
document.getElementById('totalItemsCount').textContent = allBrokenItems.length;
// Update footer stats
const footerStats = document.getElementById('modalFooterStats');
footerStats.textContent = `Total: ${allBrokenItems.length} | Filtered: ${filteredItems.length}`;
}
function populateArrFilter() {
const arrFilter = document.getElementById('arrFilterSelect');
arrFilter.innerHTML = '<option value="">All Arrs</option>';
const uniqueArrs = [...new Set(allBrokenItems.map(item => item.arr))];
uniqueArrs.forEach(arr => {
const option = document.createElement('option');
option.value = arr;
option.textContent = arr;
arrFilter.appendChild(option);
});
}
// Filter event listeners
document.getElementById('itemSearchInput').addEventListener('input', applyFilters);
document.getElementById('arrFilterSelect').addEventListener('change', applyFilters);
document.getElementById('pathFilterSelect').addEventListener('change', applyFilters);
document.getElementById('clearFiltersBtn').addEventListener('click', () => {
document.getElementById('itemSearchInput').value = '';
document.getElementById('arrFilterSelect').value = '';
document.getElementById('pathFilterSelect').value = '';
applyFilters();
});
function viewJobDetails(jobId) {
// Find the job
const job = allJobs.find(j => j.id === jobId);
if (!job) return;
currentJob = job;
selectedItems.clear();
currentItemsPage = 1;
// Prepare modal data
document.getElementById('modalJobId').textContent = job.id.substring(0, 8);
// Format dates
const startedDate = new Date(job.created_at);
document.getElementById('modalJobStarted').textContent = startedDate.toLocaleString();
if (job.finished_at) {
const completedDate = new Date(job.finished_at);
document.getElementById('modalJobCompleted').textContent = completedDate.toLocaleString();
} else {
document.getElementById('modalJobCompleted').textContent = 'N/A';
}
// Set status with color
let status = getStatus(job.status);
document.getElementById('modalJobStatus').innerHTML = `<span class="${status.class}">${status.text}</span>`;
// Set other job details
document.getElementById('modalJobArrs').textContent = job.arrs.join(', ');
document.getElementById('modalJobMediaIds').textContent = job.media_ids && job.media_ids.length > 0 ?
job.media_ids.join(', ') : 'All';
document.getElementById('modalJobAutoProcess').textContent = job.auto_process ? 'Yes' : 'No';
// Show/hide error message
const errorContainer = document.getElementById('errorContainer');
if (job.error) {
document.getElementById('modalJobError').textContent = job.error;
errorContainer.classList.remove('d-none');
} else {
errorContainer.classList.add('d-none');
}
// Process button visibility
const processBtn = document.getElementById('processJobBtn');
if (job.status === 'pending') {
processBtn.classList.remove('d-none');
processBtn.onclick = () => {
processJob(job.id);
const modal = bootstrap.Modal.getInstance(document.getElementById('jobDetailsModal'));
modal.hide();
};
} else {
processBtn.classList.add('d-none');
}
// Stop button visibility
const stopBtn = document.getElementById('stopJobBtn');
if (job.status === 'started' || job.status === 'processing') {
stopBtn.classList.remove('d-none');
stopBtn.onclick = () => {
stopJob(job.id);
const modal = bootstrap.Modal.getInstance(document.getElementById('jobDetailsModal'));
modal.hide();
};
} else {
stopBtn.classList.add('d-none');
}
// Process broken items data
if (job.broken_items && Object.entries(job.broken_items).length > 0) {
allBrokenItems = processItemsData(job.broken_items);
filteredItems = [...allBrokenItems];
populateArrFilter();
renderBrokenItemsTable();
} else {
allBrokenItems = [];
filteredItems = [];
renderBrokenItemsTable();
}
updateItemsStats();
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('jobDetailsModal'));
modal.show();
}
// Keep existing functions (selectAllJobs, updateDeleteButtonState, deleteJob, etc.)
document.getElementById('selectAllJobs').addEventListener('change', function() {
const isChecked = this.checked;
document.querySelectorAll('.job-checkbox:not(:disabled)').forEach(checkbox => {
checkbox.checked = isChecked;
});
updateDeleteButtonState();
});
function updateDeleteButtonState() {
const deleteBtn = document.getElementById('deleteSelectedJobs');
const selectedCheckboxes = document.querySelectorAll('.job-checkbox:checked');
deleteBtn.disabled = selectedCheckboxes.length === 0;
}
document.getElementById('deleteSelectedJobs').addEventListener('click', async () => {
const selectedIds = Array.from(
document.querySelectorAll('.job-checkbox:checked')
).map(checkbox => checkbox.value);
if (!selectedIds.length) return;
if (confirm(`Are you sure you want to delete ${selectedIds.length} job(s)?`)) {
await deleteMultipleJobs(selectedIds);
}
});
async function deleteJob(jobId) {
if (confirm('Are you sure you want to delete this job?')) {
try {
const response = await fetcher(`/api/repair/jobs`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: [jobId] })
});
if (!response.ok) throw new Error(await response.text());
createToast('Job deleted successfully');
await loadJobs(currentPage);
} catch (error) {
createToast(`Error deleting job: ${error.message}`, 'error');
}
}
}
async function deleteMultipleJobs(jobIds) {
try {
const response = await fetcher(`/api/repair/jobs`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: jobIds })
});
if (!response.ok) throw new Error(await response.text());
createToast(`${jobIds.length} job(s) deleted successfully`);
await loadJobs(currentPage);
} catch (error) {
createToast(`Error deleting jobs: ${error.message}`, 'error');
}
}
async function processJob(jobId) {
try {
const response = await fetcher(`/api/repair/jobs/${jobId}/process`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
});
if (!response.ok) throw new Error(await response.text());
createToast('Job processing started successfully');
await loadJobs(currentPage);
} catch (error) {
createToast(`Error processing job: ${error.message}`, 'error');
}
}
async function stopJob(jobId) {
if (confirm('Are you sure you want to stop this job?')) {
try {
const response = await fetcher(`/api/repair/jobs/${jobId}/stop`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
});
if (!response.ok) throw new Error(await response.text());
createToast('Job stop requested successfully');
await loadJobs(currentPage);
} catch (error) {
createToast(`Error stopping job: ${error.message}`, 'error');
}
}
}
document.getElementById('refreshJobs').addEventListener('click', () => {
loadJobs(currentPage);
});
// Load jobs on page load
loadJobs(1);
});
</script>
{{ end }}