Update repair tool
This commit is contained in:
@@ -25,6 +25,7 @@ type ContentFile struct {
|
|||||||
IsSymlink bool `json:"isSymlink"`
|
IsSymlink bool `json:"isSymlink"`
|
||||||
IsBroken bool `json:"isBroken"`
|
IsBroken bool `json:"isBroken"`
|
||||||
SeasonNumber int `json:"seasonNumber"`
|
SeasonNumber int `json:"seasonNumber"`
|
||||||
|
Processed bool `json:"processed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (file *ContentFile) Delete() {
|
func (file *ContentFile) Delete() {
|
||||||
|
|||||||
@@ -75,6 +75,26 @@ type Job struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *Job) getUnprocessedBrokenItems() map[string][]arr.ContentFile {
|
||||||
|
items := make(map[string][]arr.ContentFile)
|
||||||
|
|
||||||
|
for arrName, files := range j.BrokenItems {
|
||||||
|
if len(files) == 0 {
|
||||||
|
continue // Skip empty file lists
|
||||||
|
}
|
||||||
|
items[arrName] = make([]arr.ContentFile, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Path != "" && file.TargetPath != "" && !file.Processed {
|
||||||
|
items[arrName] = append(items[arrName], file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil // Return nil if no unprocessed items found
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
func New(arrs *arr.Storage, engine *debrid.Storage) *Repair {
|
func New(arrs *arr.Storage, engine *debrid.Storage) *Repair {
|
||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
workers := runtime.NumCPU() * 20
|
workers := runtime.NumCPU() * 20
|
||||||
@@ -732,7 +752,6 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
if job == nil {
|
if job == nil {
|
||||||
return fmt.Errorf("job %s not found", id)
|
return fmt.Errorf("job %s not found", id)
|
||||||
}
|
}
|
||||||
// All validation checks remain the same
|
|
||||||
if job.Status != JobPending {
|
if job.Status != JobPending {
|
||||||
return fmt.Errorf("job %s not pending", id)
|
return fmt.Errorf("job %s not pending", id)
|
||||||
}
|
}
|
||||||
@@ -746,7 +765,7 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
return fmt.Errorf("job %s already failed", id)
|
return fmt.Errorf("job %s already failed", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
brokenItems := job.BrokenItems
|
brokenItems := job.getUnprocessedBrokenItems()
|
||||||
if len(brokenItems) == 0 {
|
if len(brokenItems) == 0 {
|
||||||
r.logger.Info().Msgf("No broken items found for job %s", id)
|
r.logger.Info().Msgf("No broken items found for job %s", id)
|
||||||
job.CompletedAt = time.Now()
|
job.CompletedAt = time.Now()
|
||||||
@@ -754,63 +773,144 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.logger.Info().Msgf("Processing job %s with %d broken items", id, len(brokenItems))
|
||||||
|
go r.processJob(job, brokenItems)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repair) processJob(job *Job, brokenItems map[string][]arr.ContentFile) {
|
||||||
if job.ctx == nil || job.ctx.Err() != nil {
|
if job.ctx == nil || job.ctx.Err() != nil {
|
||||||
job.ctx, job.cancelFunc = context.WithCancel(r.ctx)
|
job.ctx, job.cancelFunc = context.WithCancel(r.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
g, ctx := errgroup.WithContext(job.ctx)
|
errs := make([]error, 0)
|
||||||
g.SetLimit(r.workers)
|
processedCount := 0
|
||||||
|
|
||||||
for arrName, items := range brokenItems {
|
for arrName, items := range brokenItems {
|
||||||
items := items
|
select {
|
||||||
arrName := arrName
|
case <-job.ctx.Done():
|
||||||
g.Go(func() error {
|
r.logger.Info().Msgf("Job %s cancelled", job.ID)
|
||||||
|
job.Status = JobCancelled
|
||||||
|
job.CompletedAt = time.Now()
|
||||||
|
job.Error = "Job was cancelled"
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Continue processing
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
a := r.arrs.Get(arrName)
|
||||||
case <-ctx.Done():
|
if a == nil {
|
||||||
return ctx.Err()
|
errs = append(errs, fmt.Errorf("arr %s not found", arrName))
|
||||||
default:
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
a := r.arrs.Get(arrName)
|
if err := a.DeleteFiles(items); err != nil {
|
||||||
if a == nil {
|
errs = append(errs, fmt.Errorf("failed to delete broken items for %s: %w", arrName, err))
|
||||||
r.logger.Error().Msgf("Arr %s not found", arrName)
|
continue
|
||||||
return nil
|
}
|
||||||
}
|
// Search for missing items
|
||||||
|
if err := a.SearchMissing(items); err != nil {
|
||||||
if err := a.DeleteFiles(items); err != nil {
|
errs = append(errs, fmt.Errorf("failed to search missing items for %s: %w", arrName, err))
|
||||||
r.logger.Error().Err(err).Msgf("Failed to delete broken items for %s", arrName)
|
continue
|
||||||
return nil
|
}
|
||||||
}
|
processedCount += len(items)
|
||||||
// Search for missing items
|
// Mark this item as processed
|
||||||
if err := a.SearchMissing(items); err != nil {
|
for i := range items {
|
||||||
r.logger.Error().Err(err).Msgf("Failed to search missing items for %s", arrName)
|
items[i].Processed = true
|
||||||
return nil
|
}
|
||||||
}
|
job.BrokenItems[arrName] = items
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update job status to in-progress
|
// Update job status to in-progress
|
||||||
job.Status = JobProcessing
|
job.Status = JobProcessing
|
||||||
r.saveToFile()
|
r.saveToFile()
|
||||||
|
|
||||||
// Launch a goroutine to wait for completion and update the job
|
if len(errs) > 0 {
|
||||||
go func() {
|
errMsg := fmt.Sprintf("Job %s encountered errors: %v", job.ID, errs)
|
||||||
if err := g.Wait(); err != nil {
|
job.Error = errMsg
|
||||||
job.FailedAt = time.Now()
|
job.FailedAt = time.Now()
|
||||||
job.Error = err.Error()
|
job.Status = JobFailed
|
||||||
job.CompletedAt = time.Now()
|
r.logger.Error().Msg(errMsg)
|
||||||
job.Status = JobFailed
|
go func() {
|
||||||
r.logger.Error().Err(err).Msgf("Job %s failed", id)
|
if err := request.SendDiscordMessage("repair_failed", "error", job.discordContext()); err != nil {
|
||||||
} else {
|
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
||||||
job.CompletedAt = time.Now()
|
}
|
||||||
job.Status = JobCompleted
|
}()
|
||||||
r.logger.Info().Msgf("Job %s completed successfully", id)
|
return
|
||||||
}
|
}
|
||||||
|
remainingItems := job.getUnprocessedBrokenItems()
|
||||||
|
if len(remainingItems) == 0 {
|
||||||
|
// All items processed, mark job as completed
|
||||||
|
job.CompletedAt = time.Now()
|
||||||
|
job.Status = JobCompleted
|
||||||
|
r.logger.Info().Msgf("Job %s completed successfully (all items processed)", job.ID)
|
||||||
|
go func() {
|
||||||
|
if err := request.SendDiscordMessage("repair_complete", "success", job.discordContext()); err != nil {
|
||||||
|
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
// Some items still remain, keep job as pending
|
||||||
|
job.Status = JobPending
|
||||||
|
r.logger.Info().Msgf("Job %s: processed %d selected items successfully, %d items remaining", job.ID, processedCount, len(remainingItems))
|
||||||
|
go func() {
|
||||||
|
if err := request.SendDiscordMessage("repair_partial_complete", "info", job.discordContext()); err != nil {
|
||||||
|
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
r.saveToFile()
|
||||||
|
}
|
||||||
|
|
||||||
r.saveToFile()
|
// ProcessJobItems processes the selected items for a job
|
||||||
}()
|
// selectedItems is the map of arr names to the list of file IDs to process
|
||||||
|
func (r *Repair) ProcessJobItems(id string, selectedItems map[string][]int) error {
|
||||||
|
job := r.GetJob(id)
|
||||||
|
if job == nil {
|
||||||
|
return fmt.Errorf("job %s not found", id)
|
||||||
|
}
|
||||||
|
if job.Status != JobPending {
|
||||||
|
return fmt.Errorf("job %s not pending", id)
|
||||||
|
}
|
||||||
|
if job.StartedAt.IsZero() {
|
||||||
|
return fmt.Errorf("job %s not started", id)
|
||||||
|
}
|
||||||
|
if !job.CompletedAt.IsZero() {
|
||||||
|
return fmt.Errorf("job %s already completed", id)
|
||||||
|
}
|
||||||
|
if !job.FailedAt.IsZero() {
|
||||||
|
return fmt.Errorf("job %s already failed", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
brokenItems := job.getUnprocessedBrokenItems()
|
||||||
|
validatedItems := make(map[string][]arr.ContentFile)
|
||||||
|
|
||||||
|
for arrName, selectedItemsList := range selectedItems {
|
||||||
|
if jobItems, exists := brokenItems[arrName]; exists {
|
||||||
|
validItems := make([]arr.ContentFile, 0, len(selectedItemsList))
|
||||||
|
for _, item := range selectedItemsList {
|
||||||
|
// Find the item in the job items
|
||||||
|
for _, jobItem := range jobItems {
|
||||||
|
if jobItem.FileId == item {
|
||||||
|
validItems = append(validItems, jobItem)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(validItems) > 0 {
|
||||||
|
validatedItems[arrName] = validItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(validatedItems) == 0 {
|
||||||
|
return fmt.Errorf("no valid items found for job %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
job.Status = JobProcessing
|
||||||
|
r.saveToFile()
|
||||||
|
|
||||||
|
go r.processJob(job, validatedItems)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,6 +326,28 @@ func (wb *Web) handleProcessRepairJob(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wb *Web) handleProcessRepairJobItems(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req struct {
|
||||||
|
Items map[string][]int `json:"items"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_store := store.Get()
|
||||||
|
if err := _store.Repair().ProcessJobItems(id, req.Items); err != nil {
|
||||||
|
wb.logger.Error().Err(err).Msg("Failed to process repair job items")
|
||||||
|
http.Error(w, "Failed to process job items: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (wb *Web) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
|
func (wb *Web) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
|
||||||
// Read ids from body
|
// Read ids from body
|
||||||
var req struct {
|
var req struct {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (wb *Web) Routes() http.Handler {
|
|||||||
r.Post("/repair", wb.handleRepairMedia)
|
r.Post("/repair", wb.handleRepairMedia)
|
||||||
r.Get("/repair/jobs", wb.handleGetRepairJobs)
|
r.Get("/repair/jobs", wb.handleGetRepairJobs)
|
||||||
r.Post("/repair/jobs/{id}/process", wb.handleProcessRepairJob)
|
r.Post("/repair/jobs/{id}/process", wb.handleProcessRepairJob)
|
||||||
|
r.Post("/repair/jobs/{id}/process-items", wb.handleProcessRepairJobItems)
|
||||||
r.Post("/repair/jobs/{id}/stop", wb.handleStopRepairJob)
|
r.Post("/repair/jobs/{id}/stop", wb.handleStopRepairJob)
|
||||||
r.Delete("/repair/jobs", wb.handleDeleteRepairJob)
|
r.Delete("/repair/jobs", wb.handleDeleteRepairJob)
|
||||||
r.Get("/torrents", wb.handleGetTorrents)
|
r.Get("/torrents", wb.handleGetTorrents)
|
||||||
|
|||||||
@@ -97,14 +97,15 @@
|
|||||||
|
|
||||||
<!-- Job Details Modal -->
|
<!-- Job Details Modal -->
|
||||||
<div class="modal fade" id="jobDetailsModal" tabindex="-1" aria-labelledby="jobDetailsModalLabel" aria-hidden="true">
|
<div class="modal fade" id="jobDetailsModal" tabindex="-1" aria-labelledby="jobDetailsModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="jobDetailsModalLabel">Job Details</h5>
|
<h5 class="modal-title" id="jobDetailsModalLabel">Job Details</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row mb-3">
|
<!-- Job Info -->
|
||||||
|
<div class="row mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<p><strong>Job ID:</strong> <span id="modalJobId"></span></p>
|
<p><strong>Job ID:</strong> <span id="modalJobId"></span></p>
|
||||||
<p><strong>Status:</strong> <span id="modalJobStatus"></span></p>
|
<p><strong>Status:</strong> <span id="modalJobStatus"></span></p>
|
||||||
@@ -122,27 +123,94 @@
|
|||||||
<strong>Error:</strong> <span id="modalJobError"></span>
|
<strong>Error:</strong> <span id="modalJobError"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6>Broken Items</h6>
|
<!-- Broken Items Section -->
|
||||||
<div class="table-responsive">
|
<div class="row">
|
||||||
<table class="table table-sm table-striped">
|
<div class="col-12">
|
||||||
<thead>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<tr>
|
<h6 class="mb-0">
|
||||||
<th>Arr</th>
|
Broken Items
|
||||||
<th>Path</th>
|
<span class="badge bg-secondary" id="totalItemsCount">0</span>
|
||||||
</tr>
|
<span class="badge bg-primary" id="selectedItemsCount">0 selected</span>
|
||||||
</thead>
|
</h6>
|
||||||
<tbody id="brokenItemsTableBody">
|
<div class="d-flex gap-2">
|
||||||
<!-- Broken items will be loaded here -->
|
<button class="btn btn-sm btn-primary" id="processSelectedItemsBtn" disabled>
|
||||||
</tbody>
|
<i class="bi bi-play-fill me-1"></i>Process Selected
|
||||||
</table>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="noBrokenItemsMessage" class="text-center py-2 d-none">
|
</div>
|
||||||
<p class="text-muted">No broken items found</p>
|
|
||||||
|
<!-- 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 style="width: 40px;">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="selectAllItemsTable">
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="modal-footer">
|
<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-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
<button type="button" class="btn btn-primary" id="processJobBtn">Process Items</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">
|
<button type="button" class="btn btn-warning d-none" id="stopJobBtn">
|
||||||
<i class="bi bi-stop-fill me-1"></i>Stop Job
|
<i class="bi bi-stop-fill me-1"></i>Stop Job
|
||||||
</button>
|
</button>
|
||||||
@@ -152,6 +220,39 @@
|
|||||||
</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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Load Arr instances
|
// Load Arr instances
|
||||||
@@ -207,11 +308,19 @@
|
|||||||
const itemsPerPage = 10;
|
const itemsPerPage = 10;
|
||||||
let allJobs = [];
|
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
|
// Load jobs function
|
||||||
async function loadJobs(page) {
|
async function loadJobs(page) {
|
||||||
try {
|
try {
|
||||||
const response = await fetcher('/api/repair/jobs');
|
const response = await fetcher('/api/repair/jobs');
|
||||||
if (!response.ok) throw new Error('Failed to fetcher jobs');
|
if (!response.ok) throw new Error('Failed to fetch jobs');
|
||||||
|
|
||||||
allJobs = await response.json();
|
allJobs = await response.json();
|
||||||
renderJobsTable(page);
|
renderJobsTable(page);
|
||||||
@@ -237,12 +346,11 @@
|
|||||||
case 'processing':
|
case 'processing':
|
||||||
return {text: 'Processing', class: 'text-info'};
|
return {text: 'Processing', class: 'text-info'};
|
||||||
default:
|
default:
|
||||||
// Return status in title case if unknown
|
|
||||||
return {text: status.charAt(0).toUpperCase() + status.slice(1), class: 'text-secondary'};
|
return {text: status.charAt(0).toUpperCase() + status.slice(1), class: 'text-secondary'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render jobs table with pagination
|
// Render jobs table with pagination (keeping existing implementation)
|
||||||
function renderJobsTable(page) {
|
function renderJobsTable(page) {
|
||||||
const tableBody = document.getElementById('jobsTableBody');
|
const tableBody = document.getElementById('jobsTableBody');
|
||||||
const paginationElement = document.getElementById('jobsPagination');
|
const paginationElement = document.getElementById('jobsPagination');
|
||||||
@@ -282,7 +390,6 @@
|
|||||||
let canDelete = job.status !== "started";
|
let canDelete = job.status !== "started";
|
||||||
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
||||||
|
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>
|
<td>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@@ -297,36 +404,35 @@
|
|||||||
<td>${totalItems}</td>
|
<td>${totalItems}</td>
|
||||||
<td>
|
<td>
|
||||||
${job.status === "pending" ?
|
${job.status === "pending" ?
|
||||||
`<button class="btn btn-sm btn-primary process-job" data-id="${job.id}">
|
`<button class="btn btn-sm btn-primary process-job" data-id="${job.id}">
|
||||||
<i class="bi bi-play-fill"></i> Process
|
<i class="bi bi-play-fill"></i> Process
|
||||||
</button>` :
|
</button>` :
|
||||||
`<button class="btn btn-sm btn-primary" disabled>
|
`<button class="btn btn-sm btn-primary" disabled>
|
||||||
<i class="bi bi-eye"></i> Process
|
<i class="bi bi-eye"></i> Process
|
||||||
</button>`
|
</button>`
|
||||||
}
|
}
|
||||||
${(job.status === "started" || job.status === "processing") ?
|
${(job.status === "started" || job.status === "processing") ?
|
||||||
`<button class="btn btn-sm btn-warning stop-job" data-id="${job.id}">
|
`<button class="btn btn-sm btn-warning stop-job" data-id="${job.id}">
|
||||||
<i class="bi bi-stop-fill"></i> Stop
|
<i class="bi bi-stop-fill"></i> Stop
|
||||||
</button>` :
|
</button>` :
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
${canDelete ?
|
${canDelete ?
|
||||||
`<button class="btn btn-sm btn-danger delete-job" data-id="${job.id}">
|
`<button class="btn btn-sm btn-danger delete-job" data-id="${job.id}">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>` :
|
</button>` :
|
||||||
`<button class="btn btn-sm btn-danger" disabled>
|
`<button class="btn btn-sm btn-danger" disabled>
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>`
|
</button>`
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create pagination
|
// Create pagination (keeping existing implementation)
|
||||||
if (totalPages > 1) {
|
if (totalPages > 1) {
|
||||||
// Previous button
|
|
||||||
const prevLi = document.createElement('li');
|
const prevLi = document.createElement('li');
|
||||||
prevLi.className = `page-item ${page === 1 ? 'disabled' : ''}`;
|
prevLi.className = `page-item ${page === 1 ? 'disabled' : ''}`;
|
||||||
prevLi.innerHTML = `<a class="page-link" href="#" aria-label="Previous" ${page !== 1 ? `data-page="${page - 1}"` : ''}>
|
prevLi.innerHTML = `<a class="page-link" href="#" aria-label="Previous" ${page !== 1 ? `data-page="${page - 1}"` : ''}>
|
||||||
@@ -334,7 +440,6 @@
|
|||||||
</a>`;
|
</a>`;
|
||||||
paginationElement.appendChild(prevLi);
|
paginationElement.appendChild(prevLi);
|
||||||
|
|
||||||
// Page numbers
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
const pageLi = document.createElement('li');
|
const pageLi = document.createElement('li');
|
||||||
pageLi.className = `page-item ${i === page ? 'active' : ''}`;
|
pageLi.className = `page-item ${i === page ? 'active' : ''}`;
|
||||||
@@ -342,7 +447,6 @@
|
|||||||
paginationElement.appendChild(pageLi);
|
paginationElement.appendChild(pageLi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next button
|
|
||||||
const nextLi = document.createElement('li');
|
const nextLi = document.createElement('li');
|
||||||
nextLi.className = `page-item ${page === totalPages ? 'disabled' : ''}`;
|
nextLi.className = `page-item ${page === totalPages ? 'disabled' : ''}`;
|
||||||
nextLi.innerHTML = `<a class="page-link" href="#" aria-label="Next" ${page !== totalPages ? `data-page="${page + 1}"` : ''}>
|
nextLi.innerHTML = `<a class="page-link" href="#" aria-label="Next" ${page !== totalPages ? `data-page="${page + 1}"` : ''}>
|
||||||
@@ -351,7 +455,7 @@
|
|||||||
paginationElement.appendChild(nextLi);
|
paginationElement.appendChild(nextLi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners to pagination
|
// Add event listeners (keeping existing implementation)
|
||||||
document.querySelectorAll('#jobsPagination a[data-page]').forEach(link => {
|
document.querySelectorAll('#jobsPagination a[data-page]').forEach(link => {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -372,7 +476,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners to action buttons
|
|
||||||
document.querySelectorAll('.process-job').forEach(button => {
|
document.querySelectorAll('.process-job').forEach(button => {
|
||||||
button.addEventListener('click', (e) => {
|
button.addEventListener('click', (e) => {
|
||||||
const jobId = e.currentTarget.dataset.id;
|
const jobId = e.currentTarget.dataset.id;
|
||||||
@@ -395,115 +498,313 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('selectAllJobs').addEventListener('change', function() {
|
// Helper functions to determine file type and format size
|
||||||
const isChecked = this.checked;
|
function getFileType(path) {
|
||||||
document.querySelectorAll('.job-checkbox:not(:disabled)').forEach(checkbox => {
|
const movieExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'];
|
||||||
checkbox.checked = isChecked;
|
const tvIndicators = ['/TV/', '/Television/', '/Series/', '/Shows/'];
|
||||||
});
|
|
||||||
updateDeleteButtonState();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to update delete button state
|
const pathLower = path.toLowerCase();
|
||||||
function updateDeleteButtonState() {
|
|
||||||
const deleteBtn = document.getElementById('deleteSelectedJobs');
|
if (tvIndicators.some(indicator => pathLower.includes(indicator.toLowerCase()))) {
|
||||||
const selectedCheckboxes = document.querySelectorAll('.job-checkbox:checked');
|
return 'tv';
|
||||||
deleteBtn.disabled = selectedCheckboxes.length === 0;
|
}
|
||||||
|
|
||||||
|
if (movieExtensions.some(ext => pathLower.endsWith(ext))) {
|
||||||
|
return pathLower.includes('/movies/') || pathLower.includes('/film') ? 'movie' : 'tv';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'other';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete selected jobs
|
function formatFileSize(bytes) {
|
||||||
document.getElementById('deleteSelectedJobs').addEventListener('click', async () => {
|
if (!bytes || bytes === 0) return 'Unknown';
|
||||||
const selectedIds = Array.from(
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
document.querySelectorAll('.job-checkbox:checked')
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
).map(checkbox => checkbox.value);
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedIds.length) return;
|
// modal functions
|
||||||
|
function processItemsData(brokenItems) {
|
||||||
|
const items = [];
|
||||||
|
let itemId = 0;
|
||||||
|
|
||||||
if (confirm(`Are you sure you want to delete ${selectedIds.length} job(s)?`)) {
|
for (const [arrName, itemsArray] of Object.entries(brokenItems)) {
|
||||||
await deleteMultipleJobs(selectedIds);
|
if (itemsArray && itemsArray.length > 0) {
|
||||||
}
|
itemsArray.forEach(item => {
|
||||||
});
|
items.push({
|
||||||
|
id: item.fileId,
|
||||||
async function deleteJob(jobId) {
|
arr: arrName,
|
||||||
if (confirm('Are you sure you want to delete this job?')) {
|
path: item.path,
|
||||||
try {
|
size: item.size || 0,
|
||||||
const response = await fetcher(`/api/repair/jobs`, {
|
type: getFileType(item.path),
|
||||||
method: 'DELETE',
|
selected: false
|
||||||
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); // Refresh the jobs list
|
|
||||||
} catch (error) {
|
|
||||||
createToast(`Error deleting job: ${error.message}`, 'error');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMultipleJobs(jobIds) {
|
function applyFilters() {
|
||||||
try {
|
const searchTerm = document.getElementById('itemSearchInput').value.toLowerCase();
|
||||||
const response = await fetcher(`/api/repair/jobs`, {
|
const arrFilter = document.getElementById('arrFilterSelect').value;
|
||||||
method: 'DELETE',
|
const pathFilter = document.getElementById('pathFilterSelect').value;
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
filteredItems = allBrokenItems.filter(item => {
|
||||||
},
|
const matchesSearch = !searchTerm || item.path.toLowerCase().includes(searchTerm);
|
||||||
body: JSON.stringify({ ids: jobIds })
|
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>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input item-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="${item.id}"
|
||||||
|
${selectedItems.has(item.id) ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Make row clickable to toggle selection
|
||||||
|
row.addEventListener('click', (e) => {
|
||||||
|
if (e.target.type !== 'checkbox') {
|
||||||
|
const checkbox = row.querySelector('.item-checkbox');
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
checkbox.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(await response.text());
|
tableBody.appendChild(row);
|
||||||
createToast(`${jobIds.length} job(s) deleted successfully`);
|
}
|
||||||
await loadJobs(currentPage); // Refresh the jobs list
|
|
||||||
} catch (error) {
|
// Add event listeners to checkboxes
|
||||||
createToast(`Error deleting jobs: ${error.message}`, 'error');
|
document.querySelectorAll('.item-checkbox').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', (e) => {
|
||||||
|
const itemId = parseInt(e.target.value);
|
||||||
|
const row = e.target.closest('tr');
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
|
selectedItems.add(itemId);
|
||||||
|
row.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
selectedItems.delete(itemId);
|
||||||
|
row.classList.remove('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateItemsStats();
|
||||||
|
updateSelectAllStates();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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">«</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">»</span>
|
||||||
|
</a>`;
|
||||||
|
paginationElement.appendChild(nextLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pagination event listeners
|
||||||
|
document.querySelectorAll('#itemsPagination a[data-items-page]').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newPage = parseInt(e.currentTarget.dataset.itemsPage);
|
||||||
|
currentItemsPage = newPage;
|
||||||
|
renderBrokenItemsTable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSelectAllStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItemsStats() {
|
||||||
|
document.getElementById('totalItemsCount').textContent = allBrokenItems.length;
|
||||||
|
document.getElementById('selectedItemsCount').textContent = `${selectedItems.size} selected`;
|
||||||
|
|
||||||
|
const processSelectedBtn = document.getElementById('processSelectedItemsBtn');
|
||||||
|
processSelectedBtn.disabled = selectedItems.size === 0;
|
||||||
|
|
||||||
|
// Update footer stats
|
||||||
|
const footerStats = document.getElementById('modalFooterStats');
|
||||||
|
footerStats.textContent = `Total: ${allBrokenItems.length} | Filtered: ${filteredItems.length} | Selected: ${selectedItems.size}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectAllStates() {
|
||||||
|
const selectAllTableCheckbox = document.getElementById('selectAllItemsTable');
|
||||||
|
const visibleCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
const checkedVisible = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
|
if (visibleCheckboxes.length === 0) {
|
||||||
|
selectAllTableCheckbox.indeterminate = false;
|
||||||
|
selectAllTableCheckbox.checked = false;
|
||||||
|
} else if (checkedVisible.length === visibleCheckboxes.length) {
|
||||||
|
selectAllTableCheckbox.indeterminate = false;
|
||||||
|
selectAllTableCheckbox.checked = true;
|
||||||
|
} else if (checkedVisible.length > 0) {
|
||||||
|
selectAllTableCheckbox.indeterminate = true;
|
||||||
|
selectAllTableCheckbox.checked = false;
|
||||||
|
} else {
|
||||||
|
selectAllTableCheckbox.indeterminate = false;
|
||||||
|
selectAllTableCheckbox.checked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process job function
|
function populateArrFilter() {
|
||||||
async function processJob(jobId) {
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('selectAllItemsTable').addEventListener('change', (e) => {
|
||||||
|
const visibleCheckboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
visibleCheckboxes.forEach(checkbox => {
|
||||||
|
const itemId = parseInt(checkbox.value);
|
||||||
|
const row = checkbox.closest('tr');
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
|
selectedItems.add(itemId);
|
||||||
|
checkbox.checked = true;
|
||||||
|
row.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
selectedItems.delete(itemId);
|
||||||
|
checkbox.checked = false;
|
||||||
|
row.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateItemsStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('processSelectedItemsBtn').addEventListener('click', async () => {
|
||||||
|
if (selectedItems.size === 0) return;
|
||||||
|
|
||||||
|
const selectedItemsData = allBrokenItems.filter(item => selectedItems.has(item.id));
|
||||||
|
|
||||||
|
// Group by arr
|
||||||
|
const itemsByArr = {};
|
||||||
|
selectedItemsData.forEach(item => {
|
||||||
|
if (!itemsByArr[item.arr]) {
|
||||||
|
itemsByArr[item.arr] = [];
|
||||||
|
}
|
||||||
|
itemsByArr[item.arr].push(item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(itemsByArr);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetcher(`/api/repair/jobs/${jobId}/process`, {
|
const response = await fetcher(`/api/repair/jobs/${currentJob.id}/process-items`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify({ items: itemsByArr })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error(await response.text());
|
if (!response.ok) throw new Error(await response.text());
|
||||||
createToast('Job processing started successfully');
|
createToast(`Processing ${selectedItems.size} selected items`);
|
||||||
await loadJobs(currentPage); // Refresh the jobs list
|
|
||||||
|
// Close modal and refresh jobs
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('jobDetailsModal'));
|
||||||
|
modal.hide();
|
||||||
|
loadJobs(currentPage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createToast(`Error processing job: ${error.message}`, 'error');
|
createToast(`Error processing selected items: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
async function stopJob(jobId) {
|
// Filter event listeners
|
||||||
if (confirm('Are you sure you want to stop this job?')) {
|
document.getElementById('itemSearchInput').addEventListener('input', applyFilters);
|
||||||
try {
|
document.getElementById('arrFilterSelect').addEventListener('change', applyFilters);
|
||||||
const response = await fetcher(`/api/repair/jobs/${jobId}/stop`, {
|
document.getElementById('pathFilterSelect').addEventListener('change', applyFilters);
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(await response.text());
|
document.getElementById('clearFiltersBtn').addEventListener('click', () => {
|
||||||
createToast('Job stop requested successfully');
|
document.getElementById('itemSearchInput').value = '';
|
||||||
await loadJobs(currentPage); // Refresh the jobs list
|
document.getElementById('arrFilterSelect').value = '';
|
||||||
} catch (error) {
|
document.getElementById('pathFilterSelect').value = '';
|
||||||
createToast(`Error stopping job: ${error.message}`, 'error');
|
applyFilters();
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View job details function
|
|
||||||
function viewJobDetails(jobId) {
|
function viewJobDetails(jobId) {
|
||||||
// Find the job
|
// Find the job
|
||||||
const job = allJobs.find(j => j.id === jobId);
|
const job = allJobs.find(j => j.id === jobId);
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
|
|
||||||
|
currentJob = job;
|
||||||
|
selectedItems.clear();
|
||||||
|
currentItemsPage = 1;
|
||||||
|
|
||||||
// Prepare modal data
|
// Prepare modal data
|
||||||
document.getElementById('modalJobId').textContent = job.id.substring(0, 8);
|
document.getElementById('modalJobId').textContent = job.id.substring(0, 8);
|
||||||
|
|
||||||
@@ -520,7 +821,6 @@
|
|||||||
|
|
||||||
// Set status with color
|
// Set status with color
|
||||||
let status = getStatus(job.status);
|
let status = getStatus(job.status);
|
||||||
|
|
||||||
document.getElementById('modalJobStatus').innerHTML = `<span class="${status.class}">${status.text}</span>`;
|
document.getElementById('modalJobStatus').innerHTML = `<span class="${status.class}">${status.text}</span>`;
|
||||||
|
|
||||||
// Set other job details
|
// Set other job details
|
||||||
@@ -552,7 +852,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop button visibility
|
// Stop button visibility
|
||||||
const stopBtn = document.getElementById('stopJobBtn'); // You'll need to add this button to the HTML
|
const stopBtn = document.getElementById('stopJobBtn');
|
||||||
if (job.status === 'started' || job.status === 'processing') {
|
if (job.status === 'started' || job.status === 'processing') {
|
||||||
stopBtn.classList.remove('d-none');
|
stopBtn.classList.remove('d-none');
|
||||||
stopBtn.onclick = () => {
|
stopBtn.onclick = () => {
|
||||||
@@ -564,46 +864,126 @@
|
|||||||
stopBtn.classList.add('d-none');
|
stopBtn.classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate broken items table
|
// Process broken items data
|
||||||
const brokenItemsTableBody = document.getElementById('brokenItemsTableBody');
|
|
||||||
const noBrokenItemsMessage = document.getElementById('noBrokenItemsMessage');
|
|
||||||
brokenItemsTableBody.innerHTML = '';
|
|
||||||
|
|
||||||
let hasBrokenItems = false;
|
|
||||||
|
|
||||||
// Check if broken_items exists and has entries
|
|
||||||
if (job.broken_items && Object.entries(job.broken_items).length > 0) {
|
if (job.broken_items && Object.entries(job.broken_items).length > 0) {
|
||||||
hasBrokenItems = true;
|
allBrokenItems = processItemsData(job.broken_items);
|
||||||
|
filteredItems = [...allBrokenItems];
|
||||||
// Loop through each Arr's broken items
|
populateArrFilter();
|
||||||
for (const [arrName, items] of Object.entries(job.broken_items)) {
|
renderBrokenItemsTable();
|
||||||
if (items && items.length > 0) {
|
|
||||||
// Add each item to the table
|
|
||||||
items.forEach(item => {
|
|
||||||
const row = document.createElement('tr');
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${arrName}</td>
|
|
||||||
<td><small class="text-muted">${item.path}</small></td>
|
|
||||||
`;
|
|
||||||
brokenItemsTableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show/hide no items message
|
|
||||||
if (hasBrokenItems) {
|
|
||||||
noBrokenItemsMessage.classList.add('d-none');
|
|
||||||
} else {
|
} else {
|
||||||
noBrokenItemsMessage.classList.remove('d-none');
|
allBrokenItems = [];
|
||||||
|
filteredItems = [];
|
||||||
|
renderBrokenItemsTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateItemsStats();
|
||||||
|
|
||||||
// Show the modal
|
// Show the modal
|
||||||
const modal = new bootstrap.Modal(document.getElementById('jobDetailsModal'));
|
const modal = new bootstrap.Modal(document.getElementById('jobDetailsModal'));
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener for refresh button
|
// 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', () => {
|
document.getElementById('refreshJobs').addEventListener('click', () => {
|
||||||
loadJobs(currentPage);
|
loadJobs(currentPage);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user