Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions web/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,20 @@ function toggleEditMode(index) {
}
lucide.createIcons();
}

// Format a byte count like core.system_utils.format_bytes (1024-based, 2 decimals,
// e.g. "42.93 GB"), so sizes computed in the browser match the ones the server renders.
// settings/cache.html declares its own GB/TB-only formatBytes() before this file loads;
// leave that one alone so its output does not change.
if (typeof window.formatBytes !== 'function') {
window.formatBytes = function formatBytes(bytes) {
var size = Number(bytes) || 0;
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = 0;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return i === 0 ? Math.round(size) + ' B' : size.toFixed(2) + ' ' + units[i];
};
}
31 changes: 31 additions & 0 deletions web/templates/maintenance/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,34 @@ <h3 id="confirm-modal-title">Confirm Action</h3>
formData.append('paths_json', JSON.stringify(Array.from(paths)));
}

// Total size of the checked boxes matching `selector`. Every checkbox is one file, so the
// size comes from its own row. A group row's data-size is the whole group (primary + children,
// used for sorting), so group rows expose their own file in data-file-size to avoid
// counting the children twice when the group checkbox selects them too.
function sumCheckedSize(selector) {
let total = 0;
document.querySelectorAll(selector + ':checked').forEach(cb => {
const row = cb.closest('tr');
if (!row) return;
const size = parseInt(row.dataset.fileSize || row.dataset.size, 10);
if (size > 0) total += size;
});
return total;
}

// Write " · 42.93 GB" next to a "Selected: N" counter (empty when no size is known).
function setSelectedSize(spanId, bytes) {
const span = document.getElementById(spanId);
if (!span) return;
// The size is only a hint: if formatting ever fails (e.g. a cached app.js from before
// formatBytes() moved there), show nothing instead of breaking the selection itself.
try {
span.textContent = bytes > 0 ? ' \u00b7 ' + formatBytes(bytes) : '';
} catch (e) {
span.textContent = '';
}
}

// Selected items tracking
let selectedUntrackedFiles = new Set();
let selectedOrphanedBackups = new Set();
Expand Down Expand Up @@ -155,6 +183,7 @@ <h3 id="confirm-modal-title">Confirm Action</h3>
if (count > 0) {
bulkActions.style.display = 'flex';
countSpan.textContent = count;
setSelectedSize('untracked-selected-size', sumCheckedSize('.untracked-checkbox'));
} else {
bulkActions.style.display = 'none';
}
Expand Down Expand Up @@ -193,6 +222,7 @@ <h3 id="confirm-modal-title">Confirm Action</h3>
if (count > 0) {
bulkActions.style.display = 'flex';
countSpan.textContent = count;
setSelectedSize('orphaned-selected-size', sumCheckedSize('.orphaned-checkbox'));

// Only show restore button if at least one orphaned (not redundant) backup is selected
if (restoreBtn) {
Expand Down Expand Up @@ -675,6 +705,7 @@ <h4 style="margin: 0 0 0.5rem 0; color: var(--plex-orange);">
if (count > 0) {
bulkActions.style.display = 'flex';
if (countSpan) countSpan.textContent = count;
setSelectedSize('dup-selected-size', sumCheckedSize('.dup-file-checkbox'));
} else {
bulkActions.style.display = 'none';
}
Expand Down
16 changes: 10 additions & 6 deletions web/templates/maintenance/partials/audit_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ <h2>Untracked Files ({{ results.untracked_files|length }}{% if results.grouped_u
These files are on cache but not managed by PlexCache. {% if is_unraid %}The Unraid mover will handle them normally.{% else %}They will not be automatically moved.{% endif %} Use the actions below to decide what happens to them:
</p>
<!-- Bulk Actions -->
<div id="untracked-bulk-actions" class="flex gap-1 mb-1" style="display: none;">
<span class="text-muted">Selected: <strong id="untracked-selected-count">0</strong></span>
<div id="untracked-bulk-actions" style="display: none; flex-direction: column; align-items: flex-start; gap: 0.5rem; margin-bottom: 0;">
<div class="flex gap-1">
<button class="btn btn-sm btn-success" onclick="keepOnCacheSelected()"
title="Keep on cache once — backs up to the array and excludes from the mover. Not a pin: won't re-cache automatically.">
<i data-lucide="hard-drive"></i> Keep on Cache
Expand All @@ -128,6 +128,8 @@ <h2>Untracked Files ({{ results.untracked_files|length }}{% if results.grouped_u
title="Move these files off the cache drive to the array, freeing cache space.">
<i data-lucide="database"></i> Move to Array
</button>
</div>
<div style="font-size: 1rem; line-height: 1.3;"><span class="text-muted">Selected:</span> <strong id="untracked-selected-count">0</strong><span id="untracked-selected-size" style="font-weight: 600;"></span></div>
</div>
</div>
<div class="card-body" style="padding: 0;">
Expand Down Expand Up @@ -155,7 +157,7 @@ <h2>Untracked Files ({{ results.untracked_files|length }}{% if results.grouped_u
{% set file = group.primary %}
{% set children = group.children %}
{% set group_size = file.size + children|sum(attribute='size') if children else file.size %}
<tr data-filename="{{ file.filename|lower }}" data-size="{{ group_size }}" data-age="{{ file.age_days }}" data-has-backup="{{ file.has_plexcached_backup|lower }}" data-has-dup="{{ file.has_array_duplicate|lower }}"{% if children %} class="untracked-group-row" onclick="toggleUntrackedGroup(this, event)" style="cursor: pointer;"{% endif %}>
<tr data-filename="{{ file.filename|lower }}" data-size="{{ group_size }}" data-file-size="{{ file.size }}" data-age="{{ file.age_days }}" data-has-backup="{{ file.has_plexcached_backup|lower }}" data-has-dup="{{ file.has_array_duplicate|lower }}"{% if children %} class="untracked-group-row" onclick="toggleUntrackedGroup(this, event)" style="cursor: pointer;"{% endif %}>
<td>
<input type="checkbox" class="untracked-checkbox" value="{{ file.cache_path }}" onchange="toggleUntrackedFile(this){% if children %}; toggleGroupCheckbox(this){% endif %}" data-has-backup="{{ file.has_plexcached_backup|lower }}" data-has-dup="{{ file.has_array_duplicate|lower }}">
</td>
Expand Down Expand Up @@ -256,15 +258,17 @@ <h2>Backup Cleanup ({{ results.orphaned_plexcached|length }})</h2>
{% endif %}
</p>
<!-- Bulk Actions -->
<div class="flex gap-1 mb-1">
<div id="orphaned-bulk-actions" class="flex gap-1" style="display: none;">
<span class="text-muted">Selected: <strong id="orphaned-selected-count">0</strong></span>
<div class="flex gap-1 mb-1" style="align-items: flex-start;">
<div id="orphaned-bulk-actions" style="display: none; flex-direction: column; align-items: flex-start; gap: 0.5rem;">
<div class="flex gap-1">
<button id="restore-selected-btn" class="btn btn-sm btn-primary" onclick="restoreOrphanedSelected(false)" style="{% if orphaned_count == 0 %}display: none;{% endif %}">
<i data-lucide="archive-restore"></i> Restore
</button>
<button class="btn btn-sm btn-danger" onclick="deleteOrphanedSelected(false)">
<i data-lucide="trash-2"></i> Delete
</button>
</div>
<div style="font-size: 1rem; line-height: 1.3;"><span class="text-muted">Selected:</span> <strong id="orphaned-selected-count">0</strong><span id="orphaned-selected-size" style="font-weight: 600;"></span></div>
</div>
<div class="flex gap-1" style="margin-left: auto;">
{% if orphaned_count > 0 %}
Expand Down
27 changes: 15 additions & 12 deletions web/templates/maintenance/partials/duplicate_card.html
Original file line number Diff line number Diff line change
Expand Up @@ -172,33 +172,36 @@
{% if scan_results.multi_version_count > 0 %}
<span class="badge badge-muted" title="Intentional 4K + HD pairs — not duplicates">{{ scan_results.multi_version_count }} multi-version</span>
{% endif %}
<span class="text-muted" style="margin-left: auto; font-size: 0.85rem;">
Scanned {{ scan_results.scanned_at | format_datetime }}
<span style="margin-left: auto; display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; column-gap: 1rem; row-gap: 0.25rem; font-size: 0.85rem;">
{% if arr_configured and scan_results.arr_enabled %}
<span style="color: var(--success); white-space: nowrap;">
<i data-lucide="check-circle" style="width: 14px; height: 14px; vertical-align: middle;"></i>
Sonarr/Radarr integration active
</span>
{% endif %}
<span class="text-muted">Scanned {{ scan_results.scanned_at | format_datetime }}</span>
</span>
</div>

{% if arr_configured and scan_results.arr_enabled %}
<p style="margin: 0 0 0.5rem 0; font-size: 0.85rem;">
<i data-lucide="check-circle" style="width: 14px; height: 14px; color: var(--success); vertical-align: middle;"></i>
<span style="color: var(--success);">Sonarr/Radarr integration active</span>
</p>
{% elif not arr_configured %}
{% if not arr_configured %}
<p style="margin: 0 0 0.5rem 0; font-size: 0.85rem;">
<i data-lucide="info" style="width: 14px; height: 14px; color: var(--plex-text-muted); vertical-align: middle;"></i>
<span class="text-muted">Configure <a href="/settings/integrations" style="color: var(--plex-orange);">Sonarr/Radarr</a> to auto-identify orphans</span>
</p>
{% endif %}

<!-- Bulk Actions -->
<div class="flex gap-1" style="flex-wrap: wrap;">
<div id="dup-bulk-actions" class="flex gap-1" style="display: none;">
<span class="text-muted">Selected: <strong id="dup-selected-count">0</strong></span>
<div class="flex gap-1" style="flex-wrap: wrap; align-items: flex-start;">
<div id="dup-bulk-actions" style="display: none; flex-direction: column; align-items: flex-start; gap: 0.5rem; margin-bottom: 0;">
<div class="flex gap-1">
<button class="btn btn-sm btn-secondary" onclick="deleteSelectedDuplicates(true)">
<i data-lucide="eye"></i> Preview
</button>
<button class="btn btn-sm btn-danger" onclick="deleteSelectedDuplicates(false)">
<i data-lucide="trash-2"></i> Delete Selected
</button>
</div>
<div style="font-size: 1rem; line-height: 1.3;"><span class="text-muted">Selected:</span> <strong id="dup-selected-count">0</strong><span id="dup-selected-size" style="font-weight: 600;"></span></div>
</div>
<div class="flex gap-1" style="margin-left: auto;">
{% if scan_results.arr_enabled and scan_results.orphan_count > 0 %}
Expand Down Expand Up @@ -294,7 +297,7 @@
</thead>
<tbody>
{% for file in item.files %}
<tr>
<tr data-size="{{ file.size }}">
<td>
{% if file.is_keeper is not true %}
<input type="checkbox" class="dup-file-checkbox" value="{{ file.fs_path }}" onchange="toggleDuplicateFile(this)">
Expand Down
Loading