Tighten Sort Unsorted matching; consolidate file management tools
Fix Sort Unsorted picking a wrong destination on words that are common clutter across many categories (a creator's name, generic marketing filler) by ranking matches on a folder's own deliberate name over incidental sibling-course words, and downweighting borrowed words that recur across many categories. Add a folder-picker/create-new/rename UI to override any proposal. Turn /unsorted into a general File Management page: move Refresh Library, Bulk Rename, and Manage Library (hide/rename) here from Settings, add a Move action to Manage Library using the same picker, and add Duplicate Courses and Clean Up Stale References tools. Add a footer link to the page from the dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+6
-466
@@ -221,101 +221,6 @@
|
||||
padding: 5px 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.curate-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-tertiary);
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.curate-row:hover {
|
||||
background: var(--bg-tertiary-hover);
|
||||
}
|
||||
.curate-row.is-hidden {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.curate-row-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.curate-row-name span.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.curate-toggle {
|
||||
font-size: 1.1em;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.curate-children {
|
||||
margin-left: 22px;
|
||||
margin-top: 4px;
|
||||
display: none;
|
||||
}
|
||||
.curate-children.expanded {
|
||||
display: block;
|
||||
}
|
||||
.curate-empty-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.curate-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.curate-rename-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius);
|
||||
padding: 4px 8px;
|
||||
font-size: 0.95em;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
.curate-row-status {
|
||||
flex-basis: 100%;
|
||||
font-size: 0.8em;
|
||||
color: var(--error);
|
||||
}
|
||||
.bulk-rename-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-tertiary);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.bulk-rename-row-names {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
.bulk-rename-old {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9em;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.bulk-rename-new {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
#save-status {
|
||||
font-size: 0.9em;
|
||||
color: var(--success);
|
||||
@@ -448,11 +353,6 @@
|
||||
<button class="btn" onclick="saveLibraryPath()">Save</button>
|
||||
</div>
|
||||
<span id="library-path-status" style="font-size: 0.85em; min-height: 1.2em;"></span>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="refreshLibraryCache()">{{ icons.icon('refresh', 14) }} Refresh Library</button>
|
||||
<span id="library-refresh-status" style="font-size: 0.85em; color: var(--text-muted);"></span>
|
||||
</div>
|
||||
<span class="setting-desc">Courses are cached briefly for speed - use this after adding or removing files directly on disk if you don't want to wait a few minutes for it to notice.</span>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">
|
||||
<button class="btn btn-secondary btn-sm" id="prewarm-durations-btn" onclick="startDurationPrewarm()">{{ icons.icon('refresh', 14) }} Precompute Lengths & Cover Art</button>
|
||||
@@ -463,50 +363,14 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Sort Unsorted Courses</h2>
|
||||
<h2>File Management</h2>
|
||||
<p class="setting-desc" style="margin-bottom: 12px;">
|
||||
Drop new courses in an "Unsorted" folder at the root of your library, then use this to
|
||||
propose where each one belongs based on your existing folder structure - nothing moves
|
||||
until you review and confirm on its own page.
|
||||
Sort new courses out of an "Unsorted" folder at the root of your library, refresh the
|
||||
library cache, bulk-rename or move things around, hide courses, check for duplicates, and
|
||||
clean up stale references - all on its own page, with room to review before anything
|
||||
changes on disk.
|
||||
</p>
|
||||
<a class="btn btn-secondary" href="/unsorted">{{ icons.icon('folder', 14) }} Open Sorter</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Manage Library</h2>
|
||||
<p class="setting-desc" style="margin-bottom: 12px;">
|
||||
Hide courses or whole folders from the Library browser without touching anything on disk
|
||||
(hiding a folder hides everything inside it), or rename a course/folder directly - no need
|
||||
to go to the NAS.
|
||||
</p>
|
||||
|
||||
<div id="hidden-list-wrap" style="margin-bottom: 15px; display: none;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px; font-size: 0.9em; color: var(--text-muted);">Currently hidden</div>
|
||||
<div id="hidden-list"></div>
|
||||
</div>
|
||||
|
||||
<div style="font-weight: 600; margin-bottom: 8px; font-size: 0.9em; color: var(--text-muted);">Browse</div>
|
||||
<div id="curate-path-bar" style="color: var(--text-muted); font-size: 13px; margin-bottom: 8px;"></div>
|
||||
<div id="curate-tree"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Bulk Rename</h2>
|
||||
<p class="setting-desc" style="margin-bottom: 12px;">
|
||||
Find and replace text across every course/folder name in the library at once - handy for
|
||||
stripping a release-group suffix off a batch of downloads. Nothing changes until you apply.
|
||||
</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px;">
|
||||
<input type="text" id="bulk-rename-find" class="hex-input"
|
||||
style="flex: 1; min-width: 140px; width: auto; font-family: var(--font-family);"
|
||||
placeholder="Find (e.g. .BOOKWARE-LERNSTUF)">
|
||||
<input type="text" id="bulk-rename-replace" class="hex-input"
|
||||
style="flex: 1; min-width: 140px; width: auto; font-family: var(--font-family);"
|
||||
placeholder="Replace with (blank to remove)">
|
||||
<button class="btn" onclick="previewBulkRename()">Preview</button>
|
||||
</div>
|
||||
<div id="bulk-rename-status" style="font-size: 0.85em; min-height: 1.2em; margin-bottom: 8px;"></div>
|
||||
<div id="bulk-rename-preview"></div>
|
||||
<a class="btn btn-secondary" href="/unsorted">{{ icons.icon('folder', 14) }} Open File Management</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -627,10 +491,6 @@
|
||||
// built dynamically here are kept in sync with the same path data
|
||||
// by hand.
|
||||
const ICON_SVGS = {
|
||||
ban: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><line x1="5.6" y1="5.6" x2="18.4" y2="18.4"></line></svg>',
|
||||
'graduation-cap': '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 9 12 4 2 9l10 5 10-5Z"></path><path d="M6 11v5c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5v-5"></path></svg>',
|
||||
folder: '<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path></svg>',
|
||||
pencil: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg>',
|
||||
check: '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
|
||||
};
|
||||
|
||||
@@ -682,309 +542,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Library curation: hide/show courses & folders ----
|
||||
function loadHiddenList() {
|
||||
fetch('/api/hidden-paths')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const wrap = document.getElementById('hidden-list-wrap');
|
||||
const list = document.getElementById('hidden-list');
|
||||
const items = data.hidden_paths || [];
|
||||
if (items.length === 0) {
|
||||
wrap.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
wrap.style.display = 'block';
|
||||
list.innerHTML = items.map(item => `
|
||||
<div class="curate-row">
|
||||
<div class="curate-row-name">
|
||||
<span>${ICON_SVGS.ban}</span>
|
||||
<span class="name">${item.name}</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="toggleHidden('${item.path.replace(/'/g, "\\'")}', false)">Show</button>
|
||||
</div>
|
||||
`).join('');
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function loadCurateLevel(path, container, isRoot) {
|
||||
const url = path ? `/library/manage?path=${encodeURIComponent(path)}` : '/library/manage';
|
||||
fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (isRoot) {
|
||||
document.getElementById('curate-path-bar').textContent = `Browsing ${data.library_path}`;
|
||||
}
|
||||
renderCurateLevel(data, container);
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<p style="color:var(--error);">Could not reach the library scanner.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderCurateLevel(data, container) {
|
||||
if (!data.items || data.items.length === 0) {
|
||||
const reason = (data.errors && data.errors.length) ? data.errors.join(' ') : 'Nothing here.';
|
||||
container.innerHTML = `<div class="curate-empty-hint">${reason}</div>`;
|
||||
return;
|
||||
}
|
||||
container.innerHTML = data.items.map(item => {
|
||||
const safePath = item.path.replace(/'/g, "\\'");
|
||||
const safeName = item.name.replace(/'/g, "\\'");
|
||||
const icon = item.type === 'course' ? ICON_SVGS['graduation-cap'] : ICON_SVGS.folder;
|
||||
const hiddenClass = item.hidden ? 'is-hidden' : '';
|
||||
const renameBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); startRename(this, '${safePath}', '${safeName}')" title="Rename">${ICON_SVGS.pencil}</button>`;
|
||||
const toggleBtn = `<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); toggleHidden('${safePath}', ${!item.hidden}, this)">${item.hidden ? 'Show' : 'Hide'}</button>`;
|
||||
const actions = `<div class="curate-actions">${renameBtn}${toggleBtn}</div>`;
|
||||
|
||||
if (item.type === 'course') {
|
||||
return `
|
||||
<div class="curate-row ${hiddenClass}">
|
||||
<div class="curate-row-name">
|
||||
<span>${icon}</span>
|
||||
<span class="name">${item.name}</span>
|
||||
</div>
|
||||
${actions}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="curate-row ${hiddenClass}" onclick="toggleCurateDir(this, '${safePath}')">
|
||||
<div class="curate-row-name">
|
||||
<span class="curate-toggle">▶</span>
|
||||
<span>${icon}</span>
|
||||
<span class="name">${item.name}</span>
|
||||
</div>
|
||||
${actions}
|
||||
</div>
|
||||
<div class="curate-children" data-loaded="false"></div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function toggleCurateDir(rowEl, path) {
|
||||
const content = rowEl.nextElementSibling;
|
||||
const toggleIcon = rowEl.querySelector('.curate-toggle');
|
||||
if (!content || !content.classList.contains('curate-children')) return;
|
||||
|
||||
if (content.classList.contains('expanded')) {
|
||||
content.classList.remove('expanded');
|
||||
if (toggleIcon) toggleIcon.textContent = '▶';
|
||||
return;
|
||||
}
|
||||
content.classList.add('expanded');
|
||||
if (toggleIcon) toggleIcon.textContent = '▼';
|
||||
|
||||
if (content.dataset.loaded === 'true') return;
|
||||
content.innerHTML = '<div class="curate-empty-hint">Loading...</div>';
|
||||
content.dataset.loaded = 'true';
|
||||
content.dataset.path = path;
|
||||
loadCurateLevel(path, content, false);
|
||||
}
|
||||
|
||||
function toggleHidden(path, hidden, btnEl) {
|
||||
fetch('/api/hidden-paths', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, hidden: hidden })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
loadHiddenList();
|
||||
// Refresh just the level this toggle happened in, rather
|
||||
// than collapsing the whole tree back to root.
|
||||
const container = btnEl ? (btnEl.closest('.curate-children') || document.getElementById('curate-tree'))
|
||||
: document.getElementById('curate-tree');
|
||||
const isRootContainer = container.id === 'curate-tree';
|
||||
const refreshPath = isRootContainer ? null : container.dataset.path;
|
||||
loadCurateLevel(refreshPath, container, isRootContainer);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// ---- Library curation: rename a course/folder in place on disk ----
|
||||
function startRename(btnEl, path, currentName) {
|
||||
const row = btnEl.closest('.curate-row');
|
||||
const nameSpan = row ? row.querySelector('.curate-row-name .name') : null;
|
||||
if (!row || !nameSpan) return;
|
||||
|
||||
let settled = false;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'curate-rename-input';
|
||||
input.value = currentName;
|
||||
input.onclick = (e) => e.stopPropagation();
|
||||
nameSpan.replaceWith(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
function cancel() {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
input.replaceWith(nameSpan);
|
||||
}
|
||||
function commit() {
|
||||
if (settled) return;
|
||||
const newName = input.value.trim();
|
||||
if (!newName || newName === currentName) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
submitRename(btnEl, path, newName, row, nameSpan, input);
|
||||
}
|
||||
input.addEventListener('keydown', (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
||||
else if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
||||
});
|
||||
input.addEventListener('blur', commit);
|
||||
}
|
||||
|
||||
function submitRename(btnEl, path, newName, row, nameSpan, input) {
|
||||
fetch('/api/rename-path', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, new_name: newName })
|
||||
})
|
||||
.then(r => r.json().then(data => ({ ok: r.ok, data })))
|
||||
.then(({ ok, data }) => {
|
||||
if (!ok || !data.success) {
|
||||
input.replaceWith(nameSpan); // nameSpan still shows the original, unchanged name
|
||||
showRowError(row, data.error || 'Rename failed');
|
||||
return;
|
||||
}
|
||||
if (data.active_course_reset) {
|
||||
alert('That was your active course - reload the main page to pick it up under its new name.');
|
||||
}
|
||||
loadHiddenList();
|
||||
const container = btnEl.closest('.curate-children') || document.getElementById('curate-tree');
|
||||
const isRootContainer = container.id === 'curate-tree';
|
||||
const refreshPath = isRootContainer ? null : container.dataset.path;
|
||||
loadCurateLevel(refreshPath, container, isRootContainer);
|
||||
})
|
||||
.catch(() => {
|
||||
input.replaceWith(nameSpan);
|
||||
showRowError(row, 'Could not reach the server');
|
||||
});
|
||||
}
|
||||
|
||||
function showRowError(row, message) {
|
||||
let status = row.querySelector('.curate-row-status');
|
||||
if (!status) {
|
||||
status = document.createElement('div');
|
||||
status.className = 'curate-row-status';
|
||||
row.appendChild(status);
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
loadHiddenList();
|
||||
loadCurateLevel(null, document.getElementById('curate-tree'), true);
|
||||
|
||||
// ---- Bulk rename: find/replace across every course/folder name ----
|
||||
let bulkRenameMatches = [];
|
||||
|
||||
function previewBulkRename() {
|
||||
const pattern = document.getElementById('bulk-rename-find').value;
|
||||
const replacement = document.getElementById('bulk-rename-replace').value;
|
||||
const status = document.getElementById('bulk-rename-status');
|
||||
const preview = document.getElementById('bulk-rename-preview');
|
||||
preview.innerHTML = '';
|
||||
if (!pattern) {
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Enter text to find first.';
|
||||
return;
|
||||
}
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'Searching...';
|
||||
fetch(`/api/bulk-rename/preview?pattern=${encodeURIComponent(pattern)}&replacement=${encodeURIComponent(replacement)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
bulkRenameMatches = data.matches || [];
|
||||
renderBulkRenamePreview();
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server.';
|
||||
});
|
||||
}
|
||||
|
||||
function renderBulkRenamePreview() {
|
||||
const status = document.getElementById('bulk-rename-status');
|
||||
const preview = document.getElementById('bulk-rename-preview');
|
||||
if (bulkRenameMatches.length === 0) {
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'No matches.';
|
||||
preview.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
status.textContent = '';
|
||||
const rows = bulkRenameMatches.map((m, i) => `
|
||||
<div class="bulk-rename-row">
|
||||
<div class="bulk-rename-row-names">
|
||||
<div class="bulk-rename-old">${m.old_name}</div>
|
||||
<div class="bulk-rename-new">→ ${m.new_name}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" onclick="removeBulkRenameMatch(${i})">Remove</button>
|
||||
</div>
|
||||
`).join('');
|
||||
const count = bulkRenameMatches.length;
|
||||
preview.innerHTML = rows +
|
||||
`<button class="btn" style="margin-top: 10px;" onclick="applyBulkRename()">Apply ${count} Rename${count === 1 ? '' : 's'}</button>`;
|
||||
}
|
||||
|
||||
function removeBulkRenameMatch(index) {
|
||||
bulkRenameMatches.splice(index, 1);
|
||||
renderBulkRenamePreview();
|
||||
}
|
||||
|
||||
function applyBulkRename() {
|
||||
if (!confirm(`Rename ${bulkRenameMatches.length} item(s)? This changes real folders on disk.`)) return;
|
||||
const status = document.getElementById('bulk-rename-status');
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'Applying...';
|
||||
fetch('/api/bulk-rename/apply', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: bulkRenameMatches })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const results = data.results || [];
|
||||
const succeeded = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success);
|
||||
const activeCourseReset = results.some(r => r.active_course_reset);
|
||||
|
||||
status.style.color = failed.length ? 'var(--error)' : 'var(--success)';
|
||||
let message = `${succeeded} renamed`;
|
||||
if (failed.length) {
|
||||
message += `, ${failed.length} failed: ` +
|
||||
failed.map(f => `${f.old_name} (${f.error})`).join('; ');
|
||||
} else {
|
||||
message += '.';
|
||||
}
|
||||
if (activeCourseReset) {
|
||||
message += ' Your active course was renamed - reload the main page to pick it up under its new name.';
|
||||
}
|
||||
status.textContent = message;
|
||||
|
||||
bulkRenameMatches = [];
|
||||
document.getElementById('bulk-rename-preview').innerHTML = '';
|
||||
document.getElementById('bulk-rename-find').value = '';
|
||||
document.getElementById('bulk-rename-replace').value = '';
|
||||
loadHiddenList();
|
||||
loadCurateLevel(null, document.getElementById('curate-tree'), true);
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server.';
|
||||
});
|
||||
}
|
||||
|
||||
function saveLibraryPath() {
|
||||
const input = document.getElementById('library_path');
|
||||
const status = document.getElementById('library-path-status');
|
||||
@@ -1010,23 +567,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function refreshLibraryCache() {
|
||||
const status = document.getElementById('library-refresh-status');
|
||||
status.style.color = 'var(--text-muted)';
|
||||
status.textContent = 'Refreshing…';
|
||||
fetch('/api/library/refresh', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
status.style.color = data.success ? 'var(--success)' : 'var(--error)';
|
||||
status.innerHTML = data.success ? (ICON_SVGS.check + ' Refreshed') : 'Could not refresh';
|
||||
if (data.success) setTimeout(() => { status.textContent = ''; }, 3000);
|
||||
})
|
||||
.catch(() => {
|
||||
status.style.color = 'var(--error)';
|
||||
status.textContent = 'Could not reach the server';
|
||||
});
|
||||
}
|
||||
|
||||
function updatePrewarmStatus(data) {
|
||||
const btn = document.getElementById('prewarm-durations-btn');
|
||||
const status = document.getElementById('prewarm-durations-status');
|
||||
|
||||
Reference in New Issue
Block a user