diff --git a/src/routes/admin/emails.tsx b/src/routes/admin/emails.tsx
index 6b81a58..b1fbb94 100644
--- a/src/routes/admin/emails.tsx
+++ b/src/routes/admin/emails.tsx
@@ -9,6 +9,7 @@ import {
import { logger } from "../../lib/logger";
import { Layout, clampText } from "./ui";
import { deleteKeysWithConcurrency } from "./helpers";
+import { emailsPageScript } from "../../scripts/generated/emails-page";
type AppEnv = { Bindings: Env };
@@ -93,320 +94,6 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
const emailAddress = `${feedId}@${env.DOMAIN}`;
const rssUrl = `https://${env.DOMAIN}/rss/${feedId}`;
- // Inline script for the emails page
- const emailsScript = `
- const EMAIL_FEED_ID = ${JSON.stringify(feedId)};
- let EMAIL_ROWS = [];
- let EMAIL_CHECKBOXES = [];
- let EMAIL_SELECTED_COUNT_EL = null;
- let EMAIL_MATCH_COUNT_EL = null;
- let EMAIL_TOTAL_COUNT_EL = null;
- let EMAIL_BULK_DELETE_BUTTON_EL = null;
- let EMAIL_SELECT_ALL_EL = null;
- let EMAIL_FILTER_TIMER = null;
- let EMAIL_BULK_DELETE_IN_PROGRESS = false;
- let EMAIL_SORT_KEY = 'receivedAt';
- let EMAIL_SORT_DIR = 'desc';
- const EMAIL_COLLATOR = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
-
- function initEmailUI() {
- EMAIL_ROWS = Array.from(document.querySelectorAll('.email-row'));
- EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
- EMAIL_SELECTED_COUNT_EL = document.getElementById('selected-email-count');
- EMAIL_MATCH_COUNT_EL = document.getElementById('email-match-count');
- EMAIL_TOTAL_COUNT_EL = document.getElementById('email-total-count');
- EMAIL_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-emails-button');
- EMAIL_SELECT_ALL_EL = document.getElementById('select-all-emails');
- setupEmailTableResizing();
- setupEmailTableSorting();
- setupEmailDeleteButtons();
- updateEmailMatchCount();
- updateEmailSelectionState();
- }
-
- function updateEmailMatchCount() {
- if (!EMAIL_MATCH_COUNT_EL) return;
- const total = EMAIL_ROWS.length;
- const visible = EMAIL_ROWS.filter((row) => !row.hidden).length;
- const query = (document.getElementById('email-search')?.value || '').trim();
- EMAIL_MATCH_COUNT_EL.textContent = query ? ('Showing ' + visible + ' of ' + total) : ('Showing ' + total);
- }
-
- function scheduleEmailFilter() {
- if (EMAIL_FILTER_TIMER) clearTimeout(EMAIL_FILTER_TIMER);
- EMAIL_FILTER_TIMER = setTimeout(filterEmailRows, 120);
- }
-
- function getEmailSortValue(row, key) {
- const prop = 'sort' + key.charAt(0).toUpperCase() + key.slice(1);
- return (row.dataset && row.dataset[prop]) ? row.dataset[prop] : '';
- }
-
- function updateEmailSortIndicators(table) {
- Array.from(table.querySelectorAll('th[data-sort-key]')).forEach((th) => {
- const key = th.getAttribute('data-sort-key') || '';
- const indicator = th.querySelector('.sort-indicator');
- const active = key === EMAIL_SORT_KEY;
- if (indicator) indicator.textContent = active ? (EMAIL_SORT_DIR === 'asc' ? '^' : 'v') : '';
- th.setAttribute('aria-sort', active ? (EMAIL_SORT_DIR === 'asc' ? 'ascending' : 'descending') : 'none');
- });
- }
-
- function sortEmailTableBy(key) {
- const table = document.querySelector('table.table-emails');
- const tbody = table ? table.querySelector('tbody') : null;
- if (!table || !tbody) return;
- if (EMAIL_SORT_KEY === key) {
- EMAIL_SORT_DIR = EMAIL_SORT_DIR === 'asc' ? 'desc' : 'asc';
- } else {
- EMAIL_SORT_KEY = key;
- EMAIL_SORT_DIR = key === 'receivedAt' ? 'desc' : 'asc';
- }
- const dir = EMAIL_SORT_DIR === 'asc' ? 1 : -1;
- const rows = Array.from(tbody.querySelectorAll('.email-row'));
- rows.sort((a, b) => dir * EMAIL_COLLATOR.compare(getEmailSortValue(a, EMAIL_SORT_KEY), getEmailSortValue(b, EMAIL_SORT_KEY)));
- const frag = document.createDocumentFragment();
- rows.forEach((row) => frag.appendChild(row));
- tbody.appendChild(frag);
- updateEmailSortIndicators(table);
- }
-
- function setupEmailTableSorting() {
- const table = document.querySelector('table.table-emails');
- if (!table) return;
- table.querySelectorAll('button.th-button[data-sort-key]').forEach((btn) => {
- btn.addEventListener('click', () => { const key = btn.getAttribute('data-sort-key'); if (key) sortEmailTableBy(key); });
- });
- updateEmailSortIndicators(table);
- }
-
- function setupEmailTableResizing() {
- const table = document.querySelector('table.table-emails');
- if (!table) return;
- const storageKey = 'email-to-rss.admin.emailsTable.colWidths';
- const minWidths = { subject: 240, receivedAt: 180, actions: 160 };
- const defaultWidths = { subject: 520, receivedAt: 220, actions: 200 };
- const cols = Array.from(table.querySelectorAll('colgroup col'));
- const colByKey = {};
- cols.forEach((col) => { const key = col.getAttribute('data-col'); if (key) colByKey[key] = col; });
- try {
- const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
- Object.keys(saved || {}).forEach((key) => { const px = Number(saved[key]); if (colByKey[key] && Number.isFinite(px)) colByKey[key].style.width = px + 'px'; });
- } catch { /* ignore */ }
- const persist = () => {
- try {
- const out = {};
- Object.keys(colByKey).forEach((key) => { if (key === 'select') return; const px = parseInt(colByKey[key].style.width || '0', 10); if (Number.isFinite(px) && px > 0) out[key] = px; });
- localStorage.setItem(storageKey, JSON.stringify(out));
- } catch { /* ignore */ }
- };
- let active = null; let rafId = 0; let pendingWidth = 0;
- table.querySelectorAll('.col-resizer').forEach((handle) => {
- handle.addEventListener('pointerdown', (event) => {
- event.preventDefault(); event.stopPropagation();
- const key = handle.getAttribute('data-col'); const col = key ? colByKey[key] : null;
- if (!key || !col) return;
- const th = handle.closest('th');
- const startWidth = th ? th.getBoundingClientRect().width : parseInt(col.style.width || '0', 10) || 200;
- active = { key, col, startX: event.clientX, startWidth };
- document.body.classList.add('is-resizing');
- handle.setPointerCapture(event.pointerId);
- });
- handle.addEventListener('pointermove', (event) => {
- if (!active) return;
- const minPx = minWidths[active.key] || 180;
- pendingWidth = Math.max(minPx, Math.round(active.startWidth + (event.clientX - active.startX)));
- if (rafId) return;
- rafId = requestAnimationFrame(() => { active.col.style.width = pendingWidth + 'px'; rafId = 0; });
- });
- const finish = () => { if (!active) return; active = null; document.body.classList.remove('is-resizing'); persist(); };
- handle.addEventListener('pointerup', finish);
- handle.addEventListener('pointercancel', finish);
- handle.addEventListener('dblclick', (event) => {
- event.preventDefault(); event.stopPropagation();
- const key = handle.getAttribute('data-col'); const col = key ? colByKey[key] : null; const px = key ? defaultWidths[key] : null;
- if (!key || !col || !px) return;
- col.style.width = px + 'px'; persist();
- });
- });
- }
-
- const EMAIL_DELETE_CONFIRM_LABEL = 'Confirm delete';
- const EMAIL_DELETE_CONFIRM_TIMEOUT_MS = 4000;
-
- function resetEmailDeleteButton(buttonEl) {
- if (!buttonEl) return;
- buttonEl.classList.remove('is-confirming');
- buttonEl.removeAttribute('data-confirming');
- buttonEl.disabled = false;
- buttonEl.textContent = buttonEl.dataset.originalLabel || 'Delete';
- }
-
- function setEmailButtonLoading(buttonEl, loading, label) {
- if (!buttonEl) return;
- if (loading) {
- if (!buttonEl.dataset.originalLabel) buttonEl.dataset.originalLabel = (buttonEl.textContent || '').trim();
- buttonEl.classList.add('is-loading');
- buttonEl.disabled = true;
- buttonEl.textContent = '';
- const spinner = document.createElement('span');
- spinner.className = 'spinner';
- spinner.setAttribute('aria-hidden', 'true');
- buttonEl.appendChild(spinner);
- buttonEl.appendChild(document.createTextNode(' ' + (label || 'Working...')));
- return;
- }
- buttonEl.classList.remove('is-loading');
- buttonEl.textContent = buttonEl.dataset.originalLabel || (buttonEl.textContent || '').trim();
- }
-
- function animateEmailRowRemoval(row, onDone) {
- if (!row) { if (onDone) onDone(); return; }
- row.classList.add('is-removing');
- window.setTimeout(() => { row.remove(); if (onDone) onDone(); }, 240);
- }
-
- async function deleteEmailRequest(emailKey, feedId) {
- const res = await fetch('/admin/emails/' + encodeURIComponent(emailKey) + '/delete?feedId=' + encodeURIComponent(feedId), {
- method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'same-origin',
- });
- const data = await res.json().catch(() => ({}));
- if (!res.ok) throw new Error(data && data.error ? String(data.error) : ('Request failed (' + res.status + ')'));
- return data;
- }
-
- function refreshEmailRowCache() {
- EMAIL_ROWS = Array.from(document.querySelectorAll('.email-row'));
- EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
- if (EMAIL_TOTAL_COUNT_EL) EMAIL_TOTAL_COUNT_EL.textContent = String(EMAIL_ROWS.length);
- updateEmailMatchCount();
- updateEmailSelectionState();
- }
-
- function setupEmailDeleteButtons() {
- Array.from(document.querySelectorAll('button[data-delete-kind="email"]')).forEach((button) => {
- if (button.dataset.deleteReady === 'true') return;
- button.dataset.deleteReady = 'true';
- button.dataset.originalLabel = (button.textContent || '').trim() || 'Delete';
- let confirming = false; let confirmTimer = 0; let inFlight = false;
- const startConfirm = () => {
- confirming = true;
- button.classList.add('is-confirming');
- button.setAttribute('data-confirming', 'true');
- button.textContent = EMAIL_DELETE_CONFIRM_LABEL;
- if (confirmTimer) window.clearTimeout(confirmTimer);
- confirmTimer = window.setTimeout(() => { confirming = false; resetEmailDeleteButton(button); }, EMAIL_DELETE_CONFIRM_TIMEOUT_MS);
- };
- button.addEventListener('click', async (event) => {
- event.preventDefault();
- if (inFlight) return;
- if (!confirming) { startConfirm(); return; }
- if (confirmTimer) window.clearTimeout(confirmTimer);
- inFlight = true;
- setEmailButtonLoading(button, true, 'Deleting...');
- const toast = window.showToast ? window.showToast('Deleting email...', { type: 'info', loading: true, duration: 0 }) : null;
- const emailKey = button.getAttribute('data-email-key') || '';
- const feedId = button.getAttribute('data-feed-id') || EMAIL_FEED_ID;
- const row = button.closest('.email-row');
- try {
- await deleteEmailRequest(emailKey, feedId);
- if (toast && toast.update) toast.update('Email deleted.', { type: 'success', loading: false, duration: 3200 });
- else if (window.showToast) window.showToast('Email deleted.', { type: 'success' });
- animateEmailRowRemoval(row, () => refreshEmailRowCache());
- } catch (error) {
- const msg = 'Delete failed: ' + (error && error.message ? error.message : 'Unknown error');
- if (toast && toast.update) toast.update(msg, { type: 'error', loading: false });
- else if (window.showToast) window.showToast(msg, { type: 'error' });
- setEmailButtonLoading(button, false); confirming = false; resetEmailDeleteButton(button);
- } finally {
- inFlight = false;
- if (!row) { setEmailButtonLoading(button, false); confirming = false; resetEmailDeleteButton(button); }
- }
- });
- button.addEventListener('keydown', (event) => {
- if (event.key === 'Escape' && confirming && !inFlight) { confirming = false; if (confirmTimer) window.clearTimeout(confirmTimer); resetEmailDeleteButton(button); }
- });
- });
- }
-
- function updateEmailSelectionState() {
- if (!EMAIL_CHECKBOXES.length) return;
- const selected = EMAIL_CHECKBOXES.filter((c) => c.checked);
- if (EMAIL_SELECTED_COUNT_EL) EMAIL_SELECTED_COUNT_EL.textContent = selected.length + ' selected';
- if (EMAIL_BULK_DELETE_BUTTON_EL) EMAIL_BULK_DELETE_BUTTON_EL.disabled = selected.length === 0;
- if (EMAIL_SELECT_ALL_EL) {
- const visible = EMAIL_CHECKBOXES.filter((c) => !c.closest('tr')?.hidden);
- EMAIL_SELECT_ALL_EL.checked = visible.length > 0 && visible.every((c) => c.checked);
- }
- }
-
- function toggleAllEmails(checked) { EMAIL_CHECKBOXES.forEach((c) => { if (!c.closest('tr')?.hidden) c.checked = checked; }); updateEmailSelectionState(); }
- function selectMatchingEmails() { EMAIL_CHECKBOXES.forEach((c) => { if (!c.closest('tr')?.hidden) c.checked = true; }); updateEmailSelectionState(); }
- function clearEmailSelection() { EMAIL_CHECKBOXES.forEach((c) => { c.checked = false; }); updateEmailSelectionState(); }
-
- function filterEmailRows() {
- const query = (document.getElementById('email-search')?.value || '').toLowerCase().trim();
- EMAIL_ROWS.forEach((row) => { row.hidden = !!query && !(row.getAttribute('data-search') || '').includes(query); });
- updateEmailMatchCount(); updateEmailSelectionState();
- }
-
- function confirmBulkEmailDelete() {
- const selected = EMAIL_CHECKBOXES.filter((c) => c.checked).length;
- if (selected === 0) return false;
- const query = (document.getElementById('email-search')?.value || '').trim();
- const extra = selected >= 200 && !query ? '\\n\\nThis is a large delete. Tip: use Search to narrow down spam first.' : '';
- return confirm('Delete ' + selected + ' selected email(s)?' + extra);
- }
-
- function removeEmailRowsByKey(emailKeys) {
- const toRemove = new Set((emailKeys || []).map((v) => String(v)));
- if (!toRemove.size) return;
- EMAIL_ROWS.forEach((row) => { const cb = row.querySelector('input.email-select'); if (cb && toRemove.has(cb.value)) row.remove(); });
- EMAIL_ROWS = Array.from(document.querySelectorAll('.email-row'));
- EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
- if (EMAIL_TOTAL_COUNT_EL) EMAIL_TOTAL_COUNT_EL.textContent = String(EMAIL_ROWS.length);
- }
-
- function onBulkEmailDeleteSubmit(event) { if (event && event.preventDefault) event.preventDefault(); void bulkDeleteSelectedEmails(); return false; }
-
- async function bulkDeleteSelectedEmails() {
- if (EMAIL_BULK_DELETE_IN_PROGRESS) return;
- const selectedKeys = EMAIL_CHECKBOXES.filter((c) => c.checked).map((c) => c.value);
- if (!selectedKeys.length) { if (window.showToast) window.showToast('No emails selected.', { type: 'info' }); return; }
- if (!confirmBulkEmailDelete()) return;
- EMAIL_BULK_DELETE_IN_PROGRESS = true;
- setEmailButtonLoading(EMAIL_BULK_DELETE_BUTTON_EL, true, 'Deleting...');
- const toast = window.showToast ? window.showToast('Deleting ' + selectedKeys.length + ' email(s)...', { type: 'info', loading: true, duration: 0 }) : null;
- const batchSize = 50; let deletedTotal = 0; const failed = [];
- try {
- const url = '/admin/feeds/' + encodeURIComponent(EMAIL_FEED_ID) + '/emails/bulk-delete';
- for (let i = 0; i < selectedKeys.length; i += batchSize) {
- const batch = selectedKeys.slice(i, i + batchSize);
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ emailKeys: batch }) });
- let data = {};
- if (window.parseJsonResponseOrThrow) { data = await window.parseJsonResponseOrThrow(res, { prefix: 'Bulk email delete failed' }); }
- else { data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data && data.error ? String(data.error) : 'Bulk email delete failed (HTTP ' + res.status + ')'); }
- const deletedKeys = Array.isArray(data.deletedEmailKeys) ? data.deletedEmailKeys : batch;
- removeEmailRowsByKey(deletedKeys); deletedTotal += deletedKeys.length;
- failed.push(...(Array.isArray(data.failedEmailKeys) ? data.failedEmailKeys : []));
- if (toast && toast.update) toast.update('Deleting... (' + Math.min(i + batch.length, selectedKeys.length) + ' of ' + selectedKeys.length + ')', { type: 'info' });
- updateEmailMatchCount(); updateEmailSelectionState();
- }
- if (toast && toast.dismiss) toast.dismiss();
- if (failed.length > 0) { if (window.showToast) window.showToast('Deleted ' + deletedTotal + ' email(s). ' + failed.length + ' failed (still visible).', { type: 'error' }); }
- else { if (window.showToast) window.showToast('Deleted ' + deletedTotal + ' email(s).', { type: 'success' }); }
- } catch (error) {
- if (toast && toast.dismiss) toast.dismiss();
- if (window.showToast) window.showToast((error && error.message) ? error.message : 'Bulk email delete failed.', { type: 'error' });
- } finally {
- EMAIL_BULK_DELETE_IN_PROGRESS = false;
- setEmailButtonLoading(EMAIL_BULK_DELETE_BUTTON_EL, false);
- updateEmailSelectionState();
- }
- }
-
- document.addEventListener('DOMContentLoaded', () => { initEmailUI(); });
- `;
return c.html(
@@ -625,7 +312,10 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
)}
-
+ {/* Config bootstrap — injects dynamic server-side data before the static compiled script */}
+
+ {/* Emails page logic compiled from src/scripts/client/emails-page.ts */}
+
,
);
});
diff --git a/src/scripts/client/emails-page.ts b/src/scripts/client/emails-page.ts
new file mode 100644
index 0000000..67a8a84
--- /dev/null
+++ b/src/scripts/client/emails-page.ts
@@ -0,0 +1,632 @@
+// Client-side script for the admin emails page.
+// Compiled by scripts/build-client.mjs — do not import DOM types from here in Worker code.
+// feedId is read at runtime from window.__APP_CONFIG__ (injected as a config bootstrap script before this one).
+
+const EMAIL_FEED_ID: string =
+ (window as unknown as { __APP_CONFIG__?: { feedId?: string } }).__APP_CONFIG__
+ ?.feedId ?? "";
+
+let EMAIL_ROWS: HTMLElement[] = [];
+let EMAIL_CHECKBOXES: HTMLInputElement[] = [];
+let EMAIL_SELECTED_COUNT_EL: HTMLElement | null = null;
+let EMAIL_MATCH_COUNT_EL: HTMLElement | null = null;
+let EMAIL_TOTAL_COUNT_EL: HTMLElement | null = null;
+let EMAIL_BULK_DELETE_BUTTON_EL: HTMLButtonElement | null = null;
+let EMAIL_SELECT_ALL_EL: HTMLInputElement | null = null;
+let EMAIL_FILTER_TIMER: ReturnType | null = null;
+let EMAIL_BULK_DELETE_IN_PROGRESS = false;
+let EMAIL_SORT_KEY = "receivedAt";
+let EMAIL_SORT_DIR = "desc";
+const EMAIL_COLLATOR = new Intl.Collator(undefined, {
+ numeric: true,
+ sensitivity: "base",
+});
+
+function initEmailUI(): void {
+ EMAIL_ROWS = Array.from(document.querySelectorAll(".email-row"));
+ EMAIL_CHECKBOXES = Array.from(
+ document.querySelectorAll(".email-select"),
+ );
+ EMAIL_SELECTED_COUNT_EL = document.getElementById("selected-email-count");
+ EMAIL_MATCH_COUNT_EL = document.getElementById("email-match-count");
+ EMAIL_TOTAL_COUNT_EL = document.getElementById("email-total-count");
+ EMAIL_BULK_DELETE_BUTTON_EL = document.getElementById(
+ "bulk-delete-emails-button",
+ ) as HTMLButtonElement | null;
+ EMAIL_SELECT_ALL_EL = document.getElementById(
+ "select-all-emails",
+ ) as HTMLInputElement | null;
+ setupEmailTableResizing();
+ setupEmailTableSorting();
+ setupEmailDeleteButtons();
+ updateEmailMatchCount();
+ updateEmailSelectionState();
+}
+
+function updateEmailMatchCount(): void {
+ if (!EMAIL_MATCH_COUNT_EL) return;
+ const total = EMAIL_ROWS.length;
+ const visible = EMAIL_ROWS.filter((row) => !row.hidden).length;
+ const query = (
+ (document.getElementById("email-search") as HTMLInputElement | null)
+ ?.value || ""
+ ).trim();
+ EMAIL_MATCH_COUNT_EL.textContent = query
+ ? "Showing " + visible + " of " + total
+ : "Showing " + total;
+}
+
+function scheduleEmailFilter(): void {
+ if (EMAIL_FILTER_TIMER) clearTimeout(EMAIL_FILTER_TIMER);
+ EMAIL_FILTER_TIMER = setTimeout(filterEmailRows, 120);
+}
+
+function getEmailSortValue(row: HTMLElement, key: string): string {
+ const prop = "sort" + key.charAt(0).toUpperCase() + key.slice(1);
+ return row.dataset && row.dataset[prop] ? row.dataset[prop]! : "";
+}
+
+function updateEmailSortIndicators(table: Element): void {
+ Array.from(table.querySelectorAll("th[data-sort-key]")).forEach(
+ (th) => {
+ const key = th.getAttribute("data-sort-key") || "";
+ const indicator = th.querySelector(".sort-indicator");
+ const active = key === EMAIL_SORT_KEY;
+ if (indicator)
+ indicator.textContent = active
+ ? EMAIL_SORT_DIR === "asc"
+ ? "^"
+ : "v"
+ : "";
+ th.setAttribute(
+ "aria-sort",
+ active
+ ? EMAIL_SORT_DIR === "asc"
+ ? "ascending"
+ : "descending"
+ : "none",
+ );
+ },
+ );
+}
+
+function sortEmailTableBy(key: string): void {
+ const table = document.querySelector("table.table-emails");
+ const tbody = table ? table.querySelector("tbody") : null;
+ if (!table || !tbody) return;
+ if (EMAIL_SORT_KEY === key) {
+ EMAIL_SORT_DIR = EMAIL_SORT_DIR === "asc" ? "desc" : "asc";
+ } else {
+ EMAIL_SORT_KEY = key;
+ EMAIL_SORT_DIR = key === "receivedAt" ? "desc" : "asc";
+ }
+ const dir = EMAIL_SORT_DIR === "asc" ? 1 : -1;
+ const rows = Array.from(tbody.querySelectorAll(".email-row"));
+ rows.sort(
+ (a, b) =>
+ dir *
+ EMAIL_COLLATOR.compare(
+ getEmailSortValue(a, EMAIL_SORT_KEY),
+ getEmailSortValue(b, EMAIL_SORT_KEY),
+ ),
+ );
+ const frag = document.createDocumentFragment();
+ rows.forEach((row) => frag.appendChild(row));
+ tbody.appendChild(frag);
+ updateEmailSortIndicators(table);
+}
+
+function setupEmailTableSorting(): void {
+ const table = document.querySelector("table.table-emails");
+ if (!table) return;
+ table
+ .querySelectorAll("button.th-button[data-sort-key]")
+ .forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const key = btn.getAttribute("data-sort-key");
+ if (key) sortEmailTableBy(key);
+ });
+ });
+ updateEmailSortIndicators(table);
+}
+
+function setupEmailTableResizing(): void {
+ const table = document.querySelector("table.table-emails");
+ if (!table) return;
+ const storageKey = "email-to-rss.admin.emailsTable.colWidths";
+ const minWidths: Record = {
+ subject: 240,
+ receivedAt: 180,
+ actions: 160,
+ };
+ const defaultWidths: Record = {
+ subject: 520,
+ receivedAt: 220,
+ actions: 200,
+ };
+ const cols = Array.from(table.querySelectorAll("colgroup col"));
+ const colByKey: Record = {};
+ cols.forEach((col) => {
+ const key = col.getAttribute("data-col");
+ if (key) colByKey[key] = col;
+ });
+ try {
+ const saved = JSON.parse(
+ localStorage.getItem(storageKey) || "{}",
+ ) as Record;
+ Object.keys(saved || {}).forEach((key) => {
+ const px = Number(saved[key]);
+ if (colByKey[key] && Number.isFinite(px))
+ colByKey[key].style.width = px + "px";
+ });
+ } catch {
+ /* ignore */
+ }
+ const persist = () => {
+ try {
+ const out: Record = {};
+ Object.keys(colByKey).forEach((key) => {
+ if (key === "select") return;
+ const px = parseInt(colByKey[key].style.width || "0", 10);
+ if (Number.isFinite(px) && px > 0) out[key] = px;
+ });
+ localStorage.setItem(storageKey, JSON.stringify(out));
+ } catch {
+ /* ignore */
+ }
+ };
+ type ActiveResize = {
+ key: string;
+ col: HTMLElement;
+ startX: number;
+ startWidth: number;
+ };
+ let active: ActiveResize | null = null;
+ let rafId = 0;
+ let pendingWidth = 0;
+ table.querySelectorAll(".col-resizer").forEach((handle) => {
+ handle.addEventListener("pointerdown", (event: PointerEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ const key = handle.getAttribute("data-col");
+ const col = key ? colByKey[key] : null;
+ if (!key || !col) return;
+ const th = handle.closest("th");
+ const startWidth = th
+ ? th.getBoundingClientRect().width
+ : parseInt(col.style.width || "0", 10) || 200;
+ active = { key, col, startX: event.clientX, startWidth };
+ document.body.classList.add("is-resizing");
+ handle.setPointerCapture(event.pointerId);
+ });
+ handle.addEventListener("pointermove", (event: PointerEvent) => {
+ if (!active) return;
+ const minPx = minWidths[active.key] || 180;
+ pendingWidth = Math.max(
+ minPx,
+ Math.round(active.startWidth + (event.clientX - active.startX)),
+ );
+ if (rafId) return;
+ rafId = requestAnimationFrame(() => {
+ active!.col.style.width = pendingWidth + "px";
+ rafId = 0;
+ });
+ });
+ const finish = () => {
+ if (!active) return;
+ active = null;
+ document.body.classList.remove("is-resizing");
+ persist();
+ };
+ handle.addEventListener("pointerup", finish);
+ handle.addEventListener("pointercancel", finish);
+ handle.addEventListener("dblclick", (event: MouseEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ const key = handle.getAttribute("data-col");
+ const col = key ? colByKey[key] : null;
+ const px = key ? defaultWidths[key] : null;
+ if (!key || !col || !px) return;
+ col.style.width = px + "px";
+ persist();
+ });
+ });
+}
+
+const EMAIL_DELETE_CONFIRM_LABEL = "Confirm delete";
+const EMAIL_DELETE_CONFIRM_TIMEOUT_MS = 4000;
+
+function resetEmailDeleteButton(buttonEl: HTMLButtonElement): void {
+ if (!buttonEl) return;
+ buttonEl.classList.remove("is-confirming");
+ buttonEl.removeAttribute("data-confirming");
+ buttonEl.disabled = false;
+ buttonEl.textContent = buttonEl.dataset.originalLabel || "Delete";
+}
+
+function setEmailButtonLoading(
+ buttonEl: HTMLButtonElement | null,
+ loading: boolean,
+ label?: string,
+): void {
+ if (!buttonEl) return;
+ if (loading) {
+ if (!buttonEl.dataset.originalLabel)
+ buttonEl.dataset.originalLabel = (buttonEl.textContent || "").trim();
+ buttonEl.classList.add("is-loading");
+ buttonEl.disabled = true;
+ buttonEl.textContent = "";
+ const spinner = document.createElement("span");
+ spinner.className = "spinner";
+ spinner.setAttribute("aria-hidden", "true");
+ buttonEl.appendChild(spinner);
+ buttonEl.appendChild(
+ document.createTextNode(" " + (label || "Working...")),
+ );
+ return;
+ }
+ buttonEl.classList.remove("is-loading");
+ buttonEl.textContent =
+ buttonEl.dataset.originalLabel || (buttonEl.textContent || "").trim();
+}
+
+function animateEmailRowRemoval(
+ row: HTMLElement | null,
+ onDone?: () => void,
+): void {
+ if (!row) {
+ if (onDone) onDone();
+ return;
+ }
+ row.classList.add("is-removing");
+ window.setTimeout(() => {
+ row.remove();
+ if (onDone) onDone();
+ }, 240);
+}
+
+async function deleteEmailRequest(
+ emailKey: string,
+ feedId: string,
+): Promise {
+ const res = await fetch(
+ "/admin/emails/" +
+ encodeURIComponent(emailKey) +
+ "/delete?feedId=" +
+ encodeURIComponent(feedId),
+ {
+ method: "POST",
+ headers: { Accept: "application/json" },
+ credentials: "same-origin",
+ },
+ );
+ const data = (await res.json().catch(() => ({}))) as Record;
+ if (!res.ok)
+ throw new Error(
+ data && data.error
+ ? String(data.error)
+ : "Request failed (" + res.status + ")",
+ );
+ return data;
+}
+
+function refreshEmailRowCache(): void {
+ EMAIL_ROWS = Array.from(document.querySelectorAll(".email-row"));
+ EMAIL_CHECKBOXES = Array.from(
+ document.querySelectorAll(".email-select"),
+ );
+ if (EMAIL_TOTAL_COUNT_EL)
+ EMAIL_TOTAL_COUNT_EL.textContent = String(EMAIL_ROWS.length);
+ updateEmailMatchCount();
+ updateEmailSelectionState();
+}
+
+interface ToastHandle {
+ update?: (msg: string, opts?: Record) => void;
+ dismiss?: () => void;
+}
+
+declare global {
+ interface Window {
+ showToast?: (msg: string, opts?: Record) => ToastHandle;
+ parseJsonResponseOrThrow?: (
+ res: Response,
+ opts?: Record,
+ ) => Promise>;
+ }
+}
+
+function setupEmailDeleteButtons(): void {
+ Array.from(
+ document.querySelectorAll(
+ 'button[data-delete-kind="email"]',
+ ),
+ ).forEach((button) => {
+ if (button.dataset.deleteReady === "true") return;
+ button.dataset.deleteReady = "true";
+ button.dataset.originalLabel =
+ (button.textContent || "").trim() || "Delete";
+ let confirming = false;
+ let confirmTimer = 0;
+ let inFlight = false;
+ const startConfirm = () => {
+ confirming = true;
+ button.classList.add("is-confirming");
+ button.setAttribute("data-confirming", "true");
+ button.textContent = EMAIL_DELETE_CONFIRM_LABEL;
+ if (confirmTimer) window.clearTimeout(confirmTimer);
+ confirmTimer = window.setTimeout(() => {
+ confirming = false;
+ resetEmailDeleteButton(button);
+ }, EMAIL_DELETE_CONFIRM_TIMEOUT_MS);
+ };
+ button.addEventListener("click", async (event: MouseEvent) => {
+ event.preventDefault();
+ if (inFlight) return;
+ if (!confirming) {
+ startConfirm();
+ return;
+ }
+ if (confirmTimer) window.clearTimeout(confirmTimer);
+ inFlight = true;
+ setEmailButtonLoading(button, true, "Deleting...");
+ const toast = window.showToast
+ ? window.showToast("Deleting email...", {
+ type: "info",
+ loading: true,
+ duration: 0,
+ })
+ : null;
+ const emailKey = button.getAttribute("data-email-key") || "";
+ const feedId = button.getAttribute("data-feed-id") || EMAIL_FEED_ID;
+ const row = button.closest(".email-row");
+ try {
+ await deleteEmailRequest(emailKey, feedId);
+ if (toast && toast.update)
+ toast.update("Email deleted.", {
+ type: "success",
+ loading: false,
+ duration: 3200,
+ });
+ else if (window.showToast)
+ window.showToast("Email deleted.", { type: "success" });
+ animateEmailRowRemoval(row, () => refreshEmailRowCache());
+ } catch (error) {
+ const msg =
+ "Delete failed: " +
+ (error instanceof Error && error.message
+ ? error.message
+ : "Unknown error");
+ if (toast && toast.update)
+ toast.update(msg, { type: "error", loading: false });
+ else if (window.showToast) window.showToast(msg, { type: "error" });
+ setEmailButtonLoading(button, false);
+ confirming = false;
+ resetEmailDeleteButton(button);
+ } finally {
+ inFlight = false;
+ if (!row) {
+ setEmailButtonLoading(button, false);
+ confirming = false;
+ resetEmailDeleteButton(button);
+ }
+ }
+ });
+ button.addEventListener("keydown", (event: KeyboardEvent) => {
+ if (event.key === "Escape" && confirming && !inFlight) {
+ confirming = false;
+ if (confirmTimer) window.clearTimeout(confirmTimer);
+ resetEmailDeleteButton(button);
+ }
+ });
+ });
+}
+
+function updateEmailSelectionState(): void {
+ if (!EMAIL_CHECKBOXES.length) return;
+ const selected = EMAIL_CHECKBOXES.filter((c) => c.checked);
+ if (EMAIL_SELECTED_COUNT_EL)
+ EMAIL_SELECTED_COUNT_EL.textContent = selected.length + " selected";
+ if (EMAIL_BULK_DELETE_BUTTON_EL)
+ EMAIL_BULK_DELETE_BUTTON_EL.disabled = selected.length === 0;
+ if (EMAIL_SELECT_ALL_EL) {
+ const visible = EMAIL_CHECKBOXES.filter(
+ (c) => !(c.closest("tr") as HTMLElement | null)?.hidden,
+ );
+ EMAIL_SELECT_ALL_EL.checked =
+ visible.length > 0 && visible.every((c) => c.checked);
+ }
+}
+
+function toggleAllEmails(checked: boolean): void {
+ EMAIL_CHECKBOXES.forEach((c) => {
+ if (!(c.closest("tr") as HTMLElement | null)?.hidden) c.checked = checked;
+ });
+ updateEmailSelectionState();
+}
+function selectMatchingEmails(): void {
+ EMAIL_CHECKBOXES.forEach((c) => {
+ if (!(c.closest("tr") as HTMLElement | null)?.hidden) c.checked = true;
+ });
+ updateEmailSelectionState();
+}
+function clearEmailSelection(): void {
+ EMAIL_CHECKBOXES.forEach((c) => {
+ c.checked = false;
+ });
+ updateEmailSelectionState();
+}
+
+function filterEmailRows(): void {
+ const query = (
+ (document.getElementById("email-search") as HTMLInputElement | null)
+ ?.value || ""
+ )
+ .toLowerCase()
+ .trim();
+ EMAIL_ROWS.forEach((row) => {
+ row.hidden =
+ !!query && !(row.getAttribute("data-search") || "").includes(query);
+ });
+ updateEmailMatchCount();
+ updateEmailSelectionState();
+}
+
+function confirmBulkEmailDelete(): boolean {
+ const selected = EMAIL_CHECKBOXES.filter((c) => c.checked).length;
+ if (selected === 0) return false;
+ const query = (
+ (document.getElementById("email-search") as HTMLInputElement | null)
+ ?.value || ""
+ ).trim();
+ const extra =
+ selected >= 200 && !query
+ ? "\n\nThis is a large delete. Tip: use Search to narrow down spam first."
+ : "";
+ return confirm("Delete " + selected + " selected email(s)?" + extra);
+}
+
+function removeEmailRowsByKey(emailKeys: string[]): void {
+ const toRemove = new Set((emailKeys || []).map((v) => String(v)));
+ if (!toRemove.size) return;
+ EMAIL_ROWS.forEach((row) => {
+ const cb = row.querySelector("input.email-select");
+ if (cb && toRemove.has(cb.value)) row.remove();
+ });
+ EMAIL_ROWS = Array.from(document.querySelectorAll(".email-row"));
+ EMAIL_CHECKBOXES = Array.from(
+ document.querySelectorAll(".email-select"),
+ );
+ if (EMAIL_TOTAL_COUNT_EL)
+ EMAIL_TOTAL_COUNT_EL.textContent = String(EMAIL_ROWS.length);
+}
+
+function onBulkEmailDeleteSubmit(event: Event): boolean {
+ if (event && event.preventDefault) event.preventDefault();
+ void bulkDeleteSelectedEmails();
+ return false;
+}
+
+async function bulkDeleteSelectedEmails(): Promise {
+ if (EMAIL_BULK_DELETE_IN_PROGRESS) return;
+ const selectedKeys = EMAIL_CHECKBOXES.filter((c) => c.checked).map(
+ (c) => c.value,
+ );
+ if (!selectedKeys.length) {
+ if (window.showToast)
+ window.showToast("No emails selected.", { type: "info" });
+ return;
+ }
+ if (!confirmBulkEmailDelete()) return;
+ EMAIL_BULK_DELETE_IN_PROGRESS = true;
+ setEmailButtonLoading(EMAIL_BULK_DELETE_BUTTON_EL, true, "Deleting...");
+ const toast = window.showToast
+ ? window.showToast("Deleting " + selectedKeys.length + " email(s)...", {
+ type: "info",
+ loading: true,
+ duration: 0,
+ })
+ : null;
+ const batchSize = 50;
+ let deletedTotal = 0;
+ const failed: string[] = [];
+ try {
+ const url =
+ "/admin/feeds/" +
+ encodeURIComponent(EMAIL_FEED_ID) +
+ "/emails/bulk-delete";
+ for (let i = 0; i < selectedKeys.length; i += batchSize) {
+ const batch = selectedKeys.slice(i, i + batchSize);
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "same-origin",
+ body: JSON.stringify({ emailKeys: batch }),
+ });
+ let data: Record = {};
+ if (window.parseJsonResponseOrThrow) {
+ data = await window.parseJsonResponseOrThrow(res, {
+ prefix: "Bulk email delete failed",
+ });
+ } else {
+ data = (await res.json().catch(() => ({}))) as Record;
+ if (!res.ok)
+ throw new Error(
+ data && data.error
+ ? String(data.error)
+ : "Bulk email delete failed (HTTP " + res.status + ")",
+ );
+ }
+ const deletedKeys = Array.isArray(data.deletedEmailKeys)
+ ? (data.deletedEmailKeys as string[])
+ : batch;
+ removeEmailRowsByKey(deletedKeys);
+ deletedTotal += deletedKeys.length;
+ failed.push(
+ ...(Array.isArray(data.failedEmailKeys)
+ ? (data.failedEmailKeys as string[])
+ : []),
+ );
+ if (toast && toast.update)
+ toast.update(
+ "Deleting... (" +
+ Math.min(i + batch.length, selectedKeys.length) +
+ " of " +
+ selectedKeys.length +
+ ")",
+ { type: "info" },
+ );
+ updateEmailMatchCount();
+ updateEmailSelectionState();
+ }
+ if (toast && toast.dismiss) toast.dismiss();
+ if (failed.length > 0) {
+ if (window.showToast)
+ window.showToast(
+ "Deleted " +
+ deletedTotal +
+ " email(s). " +
+ failed.length +
+ " failed (still visible).",
+ { type: "error" },
+ );
+ } else {
+ if (window.showToast)
+ window.showToast("Deleted " + deletedTotal + " email(s).", {
+ type: "success",
+ });
+ }
+ } catch (error) {
+ if (toast && toast.dismiss) toast.dismiss();
+ if (window.showToast)
+ window.showToast(
+ error instanceof Error && error.message
+ ? error.message
+ : "Bulk email delete failed.",
+ { type: "error" },
+ );
+ } finally {
+ EMAIL_BULK_DELETE_IN_PROGRESS = false;
+ setEmailButtonLoading(EMAIL_BULK_DELETE_BUTTON_EL, false);
+ updateEmailSelectionState();
+ }
+}
+
+// Expose functions needed by inline HTML event handlers
+(window as unknown as Record).scheduleEmailFilter =
+ scheduleEmailFilter;
+(window as unknown as Record).toggleAllEmails =
+ toggleAllEmails;
+(window as unknown as Record).selectMatchingEmails =
+ selectMatchingEmails;
+(window as unknown as Record).clearEmailSelection =
+ clearEmailSelection;
+(window as unknown as Record).onBulkEmailDeleteSubmit =
+ onBulkEmailDeleteSubmit;
+
+document.addEventListener("DOMContentLoaded", () => {
+ initEmailUI();
+});
diff --git a/src/scripts/generated/emails-page.ts b/src/scripts/generated/emails-page.ts
new file mode 100644
index 0000000..d25c173
--- /dev/null
+++ b/src/scripts/generated/emails-page.ts
@@ -0,0 +1,6 @@
+// AUTO-GENERATED by scripts/build-client.mjs — do not edit directly.
+// Source: src/scripts/client/emails-page.ts
+// Run `npm run build:client` to regenerate.
+
+export const emailsPageScript =
+ '"use strict";(()=>{var b=window.__APP_CONFIG__?.feedId??"",g=[],f=[],R=null,_=null,A=null,v=null,C=null,k=null,S=!1,T="receivedAt",p="desc",O=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"});function q(){g=Array.from(document.querySelectorAll(".email-row")),f=Array.from(document.querySelectorAll(".email-select")),R=document.getElementById("selected-email-count"),_=document.getElementById("email-match-count"),A=document.getElementById("email-total-count"),v=document.getElementById("bulk-delete-emails-button"),C=document.getElementById("select-all-emails"),K(),P(),J(),I(),w()}function I(){if(!_)return;let e=g.length,t=g.filter(i=>!i.hidden).length,n=(document.getElementById("email-search")?.value||"").trim();_.textContent=n?"Showing "+t+" of "+e:"Showing "+e}function x(){k&&clearTimeout(k),k=setTimeout(Y,120)}function H(e,t){let n="sort"+t.charAt(0).toUpperCase()+t.slice(1);return e.dataset&&e.dataset[n]?e.dataset[n]:""}function B(e){Array.from(e.querySelectorAll("th[data-sort-key]")).forEach(t=>{let n=t.getAttribute("data-sort-key")||"",i=t.querySelector(".sort-indicator"),u=n===T;i&&(i.textContent=u?p==="asc"?"^":"v":""),t.setAttribute("aria-sort",u?p==="asc"?"ascending":"descending":"none")})}function N(e){let t=document.querySelector("table.table-emails"),n=t?t.querySelector("tbody"):null;if(!t||!n)return;T===e?p=p==="asc"?"desc":"asc":(T=e,p=e==="receivedAt"?"desc":"asc");let i=p==="asc"?1:-1,u=Array.from(n.querySelectorAll(".email-row"));u.sort((l,r)=>i*O.compare(H(l,T),H(r,T)));let s=document.createDocumentFragment();u.forEach(l=>s.appendChild(l)),n.appendChild(s),B(t)}function P(){let e=document.querySelector("table.table-emails");e&&(e.querySelectorAll("button.th-button[data-sort-key]").forEach(t=>{t.addEventListener("click",()=>{let n=t.getAttribute("data-sort-key");n&&N(n)})}),B(e))}function K(){let e=document.querySelector("table.table-emails");if(!e)return;let t="email-to-rss.admin.emailsTable.colWidths",n={subject:240,receivedAt:180,actions:160},i={subject:520,receivedAt:220,actions:200},u=Array.from(e.querySelectorAll("colgroup col")),s={};u.forEach(o=>{let a=o.getAttribute("data-col");a&&(s[a]=o)});try{let o=JSON.parse(localStorage.getItem(t)||"{}");Object.keys(o||{}).forEach(a=>{let c=Number(o[a]);s[a]&&Number.isFinite(c)&&(s[a].style.width=c+"px")})}catch{}let l=()=>{try{let o={};Object.keys(s).forEach(a=>{if(a==="select")return;let c=parseInt(s[a].style.width||"0",10);Number.isFinite(c)&&c>0&&(o[a]=c)}),localStorage.setItem(t,JSON.stringify(o))}catch{}},r=null,E=0,d=0;e.querySelectorAll(".col-resizer").forEach(o=>{o.addEventListener("pointerdown",c=>{c.preventDefault(),c.stopPropagation();let m=o.getAttribute("data-col"),h=m?s[m]:null;if(!m||!h)return;let y=o.closest("th"),D=y?y.getBoundingClientRect().width:parseInt(h.style.width||"0",10)||200;r={key:m,col:h,startX:c.clientX,startWidth:D},document.body.classList.add("is-resizing"),o.setPointerCapture(c.pointerId)}),o.addEventListener("pointermove",c=>{if(!r)return;let m=n[r.key]||180;d=Math.max(m,Math.round(r.startWidth+(c.clientX-r.startX))),!E&&(E=requestAnimationFrame(()=>{r.col.style.width=d+"px",E=0}))});let a=()=>{r&&(r=null,document.body.classList.remove("is-resizing"),l())};o.addEventListener("pointerup",a),o.addEventListener("pointercancel",a),o.addEventListener("dblclick",c=>{c.preventDefault(),c.stopPropagation();let m=o.getAttribute("data-col"),h=m?s[m]:null,y=m?i[m]:null;!m||!h||!y||(h.style.width=y+"px",l())})})}var F="Confirm delete",U=4e3;function M(e){e&&(e.classList.remove("is-confirming"),e.removeAttribute("data-confirming"),e.disabled=!1,e.textContent=e.dataset.originalLabel||"Delete")}function L(e,t,n){if(e){if(t){e.dataset.originalLabel||(e.dataset.originalLabel=(e.textContent||"").trim()),e.classList.add("is-loading"),e.disabled=!0,e.textContent="";let i=document.createElement("span");i.className="spinner",i.setAttribute("aria-hidden","true"),e.appendChild(i),e.appendChild(document.createTextNode(" "+(n||"Working...")));return}e.classList.remove("is-loading"),e.textContent=e.dataset.originalLabel||(e.textContent||"").trim()}}function W(e,t){if(!e){t&&t();return}e.classList.add("is-removing"),window.setTimeout(()=>{e.remove(),t&&t()},240)}async function j(e,t){let n=await fetch("/admin/emails/"+encodeURIComponent(e)+"/delete?feedId="+encodeURIComponent(t),{method:"POST",headers:{Accept:"application/json"},credentials:"same-origin"}),i=await n.json().catch(()=>({}));if(!n.ok)throw new Error(i&&i.error?String(i.error):"Request failed ("+n.status+")");return i}function z(){g=Array.from(document.querySelectorAll(".email-row")),f=Array.from(document.querySelectorAll(".email-select")),A&&(A.textContent=String(g.length)),I(),w()}function J(){Array.from(document.querySelectorAll(\'button[data-delete-kind="email"]\')).forEach(e=>{if(e.dataset.deleteReady==="true")return;e.dataset.deleteReady="true",e.dataset.originalLabel=(e.textContent||"").trim()||"Delete";let t=!1,n=0,i=!1,u=()=>{t=!0,e.classList.add("is-confirming"),e.setAttribute("data-confirming","true"),e.textContent=F,n&&window.clearTimeout(n),n=window.setTimeout(()=>{t=!1,M(e)},U)};e.addEventListener("click",async s=>{if(s.preventDefault(),i)return;if(!t){u();return}n&&window.clearTimeout(n),i=!0,L(e,!0,"Deleting...");let l=window.showToast?window.showToast("Deleting email...",{type:"info",loading:!0,duration:0}):null,r=e.getAttribute("data-email-key")||"",E=e.getAttribute("data-feed-id")||b,d=e.closest(".email-row");try{await j(r,E),l&&l.update?l.update("Email deleted.",{type:"success",loading:!1,duration:3200}):window.showToast&&window.showToast("Email deleted.",{type:"success"}),W(d,()=>z())}catch(o){let a="Delete failed: "+(o instanceof Error&&o.message?o.message:"Unknown error");l&&l.update?l.update(a,{type:"error",loading:!1}):window.showToast&&window.showToast(a,{type:"error"}),L(e,!1),t=!1,M(e)}finally{i=!1,d||(L(e,!1),t=!1,M(e))}}),e.addEventListener("keydown",s=>{s.key==="Escape"&&t&&!i&&(t=!1,n&&window.clearTimeout(n),M(e))})})}function w(){if(!f.length)return;let e=f.filter(t=>t.checked);if(R&&(R.textContent=e.length+" selected"),v&&(v.disabled=e.length===0),C){let t=f.filter(n=>!n.closest("tr")?.hidden);C.checked=t.length>0&&t.every(n=>n.checked)}}function X(e){f.forEach(t=>{t.closest("tr")?.hidden||(t.checked=e)}),w()}function G(){f.forEach(e=>{e.closest("tr")?.hidden||(e.checked=!0)}),w()}function V(){f.forEach(e=>{e.checked=!1}),w()}function Y(){let e=(document.getElementById("email-search")?.value||"").toLowerCase().trim();g.forEach(t=>{t.hidden=!!e&&!(t.getAttribute("data-search")||"").includes(e)}),I(),w()}function Q(){let e=f.filter(i=>i.checked).length;if(e===0)return!1;let t=(document.getElementById("email-search")?.value||"").trim(),n=e>=200&&!t?`\n\nThis is a large delete. Tip: use Search to narrow down spam first.`:"";return confirm("Delete "+e+" selected email(s)?"+n)}function Z(e){let t=new Set((e||[]).map(n=>String(n)));t.size&&(g.forEach(n=>{let i=n.querySelector("input.email-select");i&&t.has(i.value)&&n.remove()}),g=Array.from(document.querySelectorAll(".email-row")),f=Array.from(document.querySelectorAll(".email-select")),A&&(A.textContent=String(g.length)))}function $(e){return e&&e.preventDefault&&e.preventDefault(),ee(),!1}async function ee(){if(S)return;let e=f.filter(s=>s.checked).map(s=>s.value);if(!e.length){window.showToast&&window.showToast("No emails selected.",{type:"info"});return}if(!Q())return;S=!0,L(v,!0,"Deleting...");let t=window.showToast?window.showToast("Deleting "+e.length+" email(s)...",{type:"info",loading:!0,duration:0}):null,n=50,i=0,u=[];try{let s="/admin/feeds/"+encodeURIComponent(b)+"/emails/bulk-delete";for(let l=0;l({})),!E.ok)throw new Error(d&&d.error?String(d.error):"Bulk email delete failed (HTTP "+E.status+")");let o=Array.isArray(d.deletedEmailKeys)?d.deletedEmailKeys:r;Z(o),i+=o.length,u.push(...Array.isArray(d.failedEmailKeys)?d.failedEmailKeys:[]),t&&t.update&&t.update("Deleting... ("+Math.min(l+r.length,e.length)+" of "+e.length+")",{type:"info"}),I(),w()}t&&t.dismiss&&t.dismiss(),u.length>0?window.showToast&&window.showToast("Deleted "+i+" email(s). "+u.length+" failed (still visible).",{type:"error"}):window.showToast&&window.showToast("Deleted "+i+" email(s).",{type:"success"})}catch(s){t&&t.dismiss&&t.dismiss(),window.showToast&&window.showToast(s instanceof Error&&s.message?s.message:"Bulk email delete failed.",{type:"error"})}finally{S=!1,L(v,!1),w()}}window.scheduleEmailFilter=x;window.toggleAllEmails=X;window.selectMatchingEmails=G;window.clearEmailSelection=V;window.onBulkEmailDeleteSubmit=$;document.addEventListener("DOMContentLoaded",()=>{q()});})();\n';