diff --git a/AGENTS.md b/AGENTS.md
index fb3ce15..ef10613 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -80,6 +80,7 @@ Notes:
- First choice: use dashboard bulk actions (`/admin`) with search + checkbox selection.
- Use **Table** view for bulk delete.
- Table columns are resizable and sortable; widths persist per-browser via localStorage.
+- **Select Results** selects all rows currently shown by the search filter; **Clear Selection** unselects everything.
- Avoid wildcard deletion; prefer search + small batches to reduce risk of deleting legitimate feeds.
## Cloudflare/Wrangler conventions
diff --git a/README.md b/README.md
index 526df80..e79f5dd 100644
--- a/README.md
+++ b/README.md
@@ -114,8 +114,8 @@ npm run build
3. Use the search box to filter obvious spam feeds.
- Long titles/URLs are truncated; hover to see the full value. Click to copy.
- Drag the column separators to resize; click headers to sort (double-click a separator to reset width).
-4. Select rows and use **Delete Selected Feeds**.
-5. For legitimate feeds that got spam emails, open **Emails**, filter by subject, then **Delete Selected Emails**.
+4. Use **Select Results** to select the filtered rows, then click **Delete Selected**.
+5. For legitimate feeds that got spam emails, open **Emails**, filter by subject, then **Select Results** and **Delete Selected**.
## Upgrading dependencies
diff --git a/src/routes/admin.ts b/src/routes/admin.ts
index a64713a..b535e96 100644
--- a/src/routes/admin.ts
+++ b/src/routes/admin.ts
@@ -352,35 +352,6 @@ app.get("/", async (c) => {
: view === "table"
? html`
`
@@ -810,6 +810,7 @@ app.get("/", async (c) => {
let FEED_ROWS = [];
let FEED_CHECKBOXES = [];
let FEED_SELECTED_COUNT_EL = null;
+ let FEED_MATCH_COUNT_EL = null;
let FEED_BULK_DELETE_BUTTON_EL = null;
let FEED_SELECT_ALL_EL = null;
let FEED_FILTER_TIMER = null;
@@ -821,13 +822,23 @@ app.get("/", async (c) => {
FEED_ROWS = Array.from(document.querySelectorAll('.feed-row'));
FEED_CHECKBOXES = Array.from(document.querySelectorAll('.feed-select'));
FEED_SELECTED_COUNT_EL = document.getElementById('selected-feed-count');
+ FEED_MATCH_COUNT_EL = document.getElementById('feed-match-count');
FEED_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-feeds-button');
FEED_SELECT_ALL_EL = document.getElementById('select-all-feeds');
setupFeedTableResizing();
setupFeedTableSorting();
+ updateFeedMatchCount();
updateFeedSelectionState();
}
+ function updateFeedMatchCount() {
+ if (!FEED_MATCH_COUNT_EL) return;
+ const total = FEED_ROWS.length;
+ const visible = FEED_ROWS.filter((row) => !row.hidden).length;
+ const query = (document.getElementById('feed-search')?.value || '').trim();
+ FEED_MATCH_COUNT_EL.textContent = query ? ('Showing ' + visible + ' of ' + total) : ('Showing ' + total);
+ }
+
function scheduleFeedFilter() {
if (FEED_FILTER_TIMER) {
clearTimeout(FEED_FILTER_TIMER);
@@ -1052,12 +1063,24 @@ app.get("/", async (c) => {
updateFeedSelectionState();
}
+ function selectMatchingFeeds() {
+ setVisibleFeedSelection(true);
+ }
+
+ function clearFeedSelection() {
+ FEED_CHECKBOXES.forEach((checkbox) => {
+ checkbox.checked = false;
+ })
+ updateFeedSelectionState();
+ }
+
function filterFeedRows() {
const query = (document.getElementById('feed-search')?.value || '').toLowerCase().trim();
FEED_ROWS.forEach((row) => {
const haystack = row.getAttribute('data-search') || '';
row.hidden = !!query && !haystack.includes(query);
});
+ updateFeedMatchCount();
updateFeedSelectionState();
}
@@ -1520,38 +1543,51 @@ app.get("/feeds/:feedId/emails", async (c) => {
: ""}
${feedMetadata.emails.length > 0
? html`
-
-
`
: html`
@@ -1663,32 +1689,43 @@ app.get("/feeds/:feedId/emails", async (c) => {