fix(web): add comic delete UI and fix container Cloudflare bypass for #6
- Add delete button (SVG X, hover-reveal) and confirmation modal to comic cards - Add DELETE /api/comics/delete endpoint with path traversal protection - Fix container downloads: delegate Cloudflare-blocked requests to FlareSolverr (headless Chrome sidecar) instead of retrying with Go HTTP client, whose Linux TCP fingerprint is flagged by Cloudflare even with network_mode: host - Add FlareSolverr service to docker-compose; inject FLARESOLVERR_URL env var - Add diagnostic logging to BatcaveBizMarkup request flow - Trim URL whitespace before storing in download job - Guard Archive() against empty filelist; fix runJob error-check ordering
This commit is contained in:
@@ -419,6 +419,33 @@
|
||||
|
||||
.comic-card:hover .comic-download-overlay { opacity: 1; }
|
||||
|
||||
.comic-delete-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
background: rgba(10,12,20,0.75);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
color: var(--text2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
||||
z-index: 4;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.comic-card:hover .comic-delete-btn { opacity: 1; }
|
||||
.comic-delete-btn:hover { background: var(--error); color: #fff; border-color: transparent; }
|
||||
|
||||
.comic-cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -998,6 +1025,21 @@
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<div class="modal-backdrop" id="delete-modal">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-modal-title">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="delete-modal-title">Delete comic?</span>
|
||||
<button class="modal-close" id="delete-close-btn" aria-label="Close">×</button>
|
||||
</div>
|
||||
<p style="color:var(--text2);margin:0 0 20px;">This will permanently remove <strong id="delete-comic-name" style="color:var(--text)"></strong> and all its files from the library.</p>
|
||||
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||
<button class="pick-btn" id="delete-cancel-btn" type="button">Cancel</button>
|
||||
<button class="pick-btn" id="delete-confirm-btn" style="background:var(--error);border-color:var(--error);color:#fff;">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden file inputs for upload modal -->
|
||||
<input type="file" id="file-input-folder" style="display:none" multiple webkitdirectory />
|
||||
<input type="file" id="file-input-files" style="display:none" multiple accept="image/*" />
|
||||
@@ -1243,6 +1285,18 @@
|
||||
|
||||
info.append(title);
|
||||
a.append(info);
|
||||
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.className = 'comic-delete-btn';
|
||||
delBtn.title = 'Delete comic';
|
||||
delBtn.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
||||
delBtn.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openDeleteModal(comic.title);
|
||||
});
|
||||
a.append(delBtn);
|
||||
|
||||
grid.append(a);
|
||||
});
|
||||
}
|
||||
@@ -1357,6 +1411,46 @@
|
||||
setInterval(pollJobs, 2000);
|
||||
setInterval(fetchComics, 10000);
|
||||
|
||||
// ── Delete modal ───────────────────────────────────────────────────────
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
const deleteCloseBtn = document.getElementById('delete-close-btn');
|
||||
const deleteCancelBtn = document.getElementById('delete-cancel-btn');
|
||||
const deleteConfirmBtn = document.getElementById('delete-confirm-btn');
|
||||
const deleteComicName = document.getElementById('delete-comic-name');
|
||||
|
||||
let pendingDeleteTitle = null;
|
||||
|
||||
function openDeleteModal(title) {
|
||||
pendingDeleteTitle = title;
|
||||
deleteComicName.textContent = title;
|
||||
deleteModal.classList.add('open');
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
deleteModal.classList.remove('open');
|
||||
pendingDeleteTitle = null;
|
||||
}
|
||||
|
||||
deleteCloseBtn.addEventListener('click', closeDeleteModal);
|
||||
deleteCancelBtn.addEventListener('click', closeDeleteModal);
|
||||
deleteModal.addEventListener('click', e => { if (e.target === deleteModal) closeDeleteModal(); });
|
||||
|
||||
deleteConfirmBtn.addEventListener('click', async () => {
|
||||
if (!pendingDeleteTitle) return;
|
||||
const title = pendingDeleteTitle;
|
||||
closeDeleteModal();
|
||||
try {
|
||||
const res = await fetch('/api/comics/delete', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
if (res.ok || res.status === 204) {
|
||||
await fetchComics();
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
// ── Upload modal ───────────────────────────────────────────────────────
|
||||
const uploadModal = document.getElementById('upload-modal');
|
||||
const uploadOpenBtn = document.getElementById('upload-open-btn');
|
||||
|
||||
Reference in New Issue
Block a user