webui: RSS manual download / repull / rule-run controls

- Per-article Add button now sends the article key and reflects the
  grabbed state (✓ Added, disabled).
- Per-feed ⟳ repull plus a Refresh-all button.
- Per-rule ⏵ "Run now" that re-applies the rule to existing articles
  and reports how many were added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-23 01:45:49 -04:00
parent ed906a3549
commit 0aeb039586
2 changed files with 27 additions and 3 deletions

View file

@ -47,6 +47,8 @@ export const api = {
deleteFeed: (name) => jpost('/api/rss/delete', { name }),
saveRssRule: (rule) => jpost('/api/rss/rules', rule),
deleteRssRule: (name) => jpost('/api/rss/rules/delete', { name }),
runRssRule: (name) => jpost('/api/rss/rules/run', { name }),
refreshFeeds: (name) => jpost('/api/rss/refresh', name ? { name } : {}),
rssDownload: (item) => jpost('/api/rss/download', item),
saveIndexer: (ix) => jpost('/api/indexers', ix),
deleteIndexer: (name) => jpost('/api/indexers/delete', { name }),

View file

@ -962,10 +962,13 @@ async function renderRssView(host, seq) {
host.innerHTML = `<div class="pane"><div class="split2">
<div>
<div class="card">
<div class="card-head"><h3>Feeds</h3><button class="btn" id="addFeedBtn"> Add feed</button></div>
<div class="card-head"><h3>Feeds</h3><span>
<button class="btn" id="refreshAllBtn" title="Re-fetch all feeds now"> Refresh all</button>
<button class="btn" id="addFeedBtn"> Add feed</button></span></div>
${feeds.length ? feeds.map((fd) => `<div class="feed-row" data-feed="${f.esc(fd.name)}">
<div class="kvrow"><span style="flex:1">📡 <b>${f.esc(fd.name)}</b> <span class="dim">(${fd.articles.length})</span></span>
<span class="dim">${fd.lastUpdate ? f.ago(fd.lastUpdate) : 'not fetched yet'}</span>
<button class="iconbtn repull-feed" title="Re-fetch this feed now"></button>
<button class="iconbtn del-feed" title="Remove feed"></button></div>
<div class="kvrow dim"><code class="inline">${f.esc(fd.url)}</code></div>
</div>`).join('<div style="height:8px"></div>') : '<div class="dim">No feeds yet. Add one to start polling.</div>'}
@ -977,7 +980,9 @@ async function renderRssView(host, seq) {
${articles.map((a, i) => `<tr style="${a.isRead ? 'opacity:.5' : ''}">
<td class="dim">${f.esc(a.feed)}</td><td>${f.esc(a.title)}</td>
<td class="num">${a.size ? f.bytes(a.size) : '—'}</td><td class="num dim">${f.esc(a.pubDate || '')}</td>
<td>${(a.magnet || a.torrentUrl) ? `<button class="btn dl-art" data-art="${i}"> Add</button>` : '<span class="faint">—</span>'}</td></tr>`).join('')}
<td>${(a.magnet || a.torrentUrl)
? `<button class="btn dl-art" data-art="${i}"${a.grabbed ? ' disabled' : ''}>${a.grabbed ? '✓ Added' : ' Add'}</button>`
: '<span class="faint">—</span>'}</td></tr>`).join('')}
</tbody></table>` : '<div class="dim">No articles yet.</div>'}
</div>
<div class="card-head" style="margin-top:16px"><h3>Auto-download rules</h3><button class="btn" id="addRuleBtn"> New rule</button></div>
@ -987,6 +992,17 @@ async function renderRssView(host, seq) {
document.getElementById('addFeedBtn').addEventListener('click', openAddFeed);
document.getElementById('addRuleBtn').addEventListener('click', () => openRuleEditor(null, feeds));
document.getElementById('refreshAllBtn').addEventListener('click', (e) => guard(async () => {
e.target.disabled = true; e.target.textContent = '⟳ Refreshing…';
await api.refreshFeeds();
renderView();
}, 'Failed to refresh feeds'));
host.querySelectorAll('.repull-feed').forEach((b) => b.addEventListener('click', () => guard(async () => {
const name = b.closest('.feed-row').dataset.feed;
b.disabled = true;
await api.refreshFeeds(name);
renderView();
}, 'Failed to refresh feed')));
host.querySelectorAll('.del-feed').forEach((b) => b.addEventListener('click', async () => {
const name = b.closest('.feed-row').dataset.feed;
await guard(() => api.deleteFeed(name), 'Failed to remove feed');
@ -994,11 +1010,16 @@ async function renderRssView(host, seq) {
}));
host.querySelectorAll('.dl-art').forEach((b) => b.addEventListener('click', () => guard(async () => {
const a = articles[+b.dataset.art];
await api.rssDownload({ magnet: a.magnet || '', torrentUrl: a.torrentUrl || '' });
await api.rssDownload({ magnet: a.magnet || '', torrentUrl: a.torrentUrl || '', key: a.key || '' });
b.textContent = '✓ Added'; b.disabled = true;
}, 'Failed to add torrent')));
host.querySelectorAll('[data-rule-edit]').forEach((b) => b.addEventListener('click', () =>
openRuleEditor(rules.find((r) => r.name === b.dataset.ruleEdit), feeds)));
host.querySelectorAll('[data-rule-run]').forEach((b) => b.addEventListener('click', () => guard(async () => {
const res = await api.runRssRule(b.dataset.ruleRun);
toast(`Rule run: ${res.grabbed} added of ${res.matched} match(es)`, 'ok');
renderView();
}, 'Failed to run rule')));
host.querySelectorAll('[data-rule-del]').forEach((b) => b.addEventListener('click', async () => {
await guard(() => api.deleteRssRule(b.dataset.ruleDel), 'Failed to delete rule');
renderView();
@ -1074,6 +1095,7 @@ function renderRule(r) {
${r.useRegex ? '<span class="pill">regex</span>' : ''}
${r.addPaused ? '<span class="pill paused">add paused</span>' : ''}
<span style="flex:1"></span>
<button class="iconbtn" data-rule-run="${f.esc(r.name)}" title="Run now against existing articles"></button>
<button class="iconbtn" data-rule-edit="${f.esc(r.name)}" title="Edit rule"></button>
<button class="iconbtn" data-rule-del="${f.esc(r.name)}" title="Delete rule"></button>
</div>