From 84b4203daf7f18980c8e04091ed7e4b2704eb766 Mon Sep 17 00:00:00 2001 From: rays Date: Tue, 20 Oct 2026 15:37:19 +0000 Subject: [PATCH] A feed's page follows its row after a check (#316) The header was drawn once, on opening the feed. feed_row put the new row in S.feeds and redrew the feed list but not the open page, so a check that cleared an error left the error showing, with the old "checked" time, until the feed was opened again. Seen on Eli Bendersky's website: a connect timeout at 15:13, a clean forced check at 15:28, and the timeout still on the page. Only the stat line and the error banner are redrawn: the whole page would close the open item. Co-Authored-By: Claude Opus 5.5 --- tests/ui/app.spec.js | 11 +++++++++++ web/CHANGELOG.md | 1 + web/src/feedpage.ts | 21 ++++++++++++++++++--- web/src/feeds.ts | 5 +++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 6111626..2039afa 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1906,6 +1906,17 @@ test('a refresh button turns from the press to the end of the scan it asked for' expect(await spin('#scanAll .i')).toBe('none'); }); +test("an open feed's page follows its row: an error a later check clears goes", async ({ page }) => { + // The header was drawn once, on opening, and kept a cleared error until reopened (#316). + await page.locator('.feed', { hasText: 'Test Show' }).click(); + const head = page.locator('.fhead .meta'); + await page.evaluate(() => patchFeed({ ...S.feeds.find(f => f.id === 'test-show'), last_error: 'connecting: timed out', failing: null })); + await expect(head.locator('.fail')).toHaveText('connecting: timed out'); + await page.evaluate(() => patchFeed({ ...S.feeds.find(f => f.id === 'test-show'), last_error: null, entries: 99 })); + await expect(head.locator('.fail')).toHaveCount(0); + await expect(head.locator('.stat')).toContainText('99 items'); +}); + test.describe('with motion reduced', () => { test.use({ reducedMotion: 'reduce' }); diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index d47e200..2c1eb6a 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Scrolling to the end of a dialog or the phone's feed list no longer scrolls the page behind it (#312). - Cover artwork has a faint edge, so a dark cover stands off a dark theme (#313). - The tab bar's corners follow the tabs inside it (#314), and the Directory's covers fan out smoothly (#315). +- A feed's page drops an error once a later check succeeds, and its item count and when it was last checked stay current, instead of keeping what they were when you opened it (#316). - Settings shows iPX's and the page's versions at its foot: the logo's tooltip was out of reach of a keyboard and of a phone (#309). ## [1.2.0] - 2026-10-19 diff --git a/web/src/feedpage.ts b/web/src/feedpage.ts index 53fa97c..42629dd 100644 --- a/web/src/feedpage.ts +++ b/web/src/feedpage.ts @@ -1,4 +1,21 @@ /* ---------------- feed page ---------------- */ +function statHTML(f: Feed){ + return `
${ + plural(f.entries,'item')}, ${f.downloaded} downloaded · checked ${ago(f.last_checked)}${ + f.subscribers>1?` · shared with ${f.subscribers-1} other ${f.subscribers===2?'person':'people'}`:''}
`; +} +/// The open feed's counts, last check and error, from a row that came after its page was drawn: +/// the page kept an error a later check had cleared (#316). Not the whole page, which would close +/// the open item. +function patchFeedHead(f: Feed){ + const meta=document.querySelector('#content .fhead .meta'); if(!meta) return; + meta.querySelector('.fail')?.remove(); + const stat=meta.querySelector('.stat'); if(!stat) return; + // An OPML's header has a stat line of its own. + if(!S.feeds.some(c=>c.group===f.id)) stat.outerHTML=statHTML(f); + meta.querySelector('.stat')!.insertAdjacentHTML('afterend',failBannerHTML(f)); + wireFailBanner(meta,f); +} function renderFeed(){ paintScanning(); // another feed's page may be the one open now const box=$('#content'); @@ -20,9 +37,7 @@ function renderFeed(){ ${artHTML(f.image,name)}

${esc(name)}

-
${ - plural(f.entries,'item')}, ${f.downloaded} downloaded · checked ${ago(f.last_checked)}${ - f.subscribers>1?` · shared with ${f.subscribers-1} other ${f.subscribers===2?'person':'people'}`:''}
+ ${statHTML(f)} ${failBannerHTML(f)} ${f.orphaned?`
This feed is no longer listed in its OPML subscription. It was kept rather than removed because it has downloaded items.
`:''} diff --git a/web/src/feeds.ts b/web/src/feeds.ts index 8881e52..4c546bd 100644 --- a/web/src/feeds.ts +++ b/web/src/feeds.ts @@ -5,6 +5,7 @@ let rowsQueued=0; function patchFeed(row: Feed){ const i=S.feeds.findIndex(f=>f.id===row.id); if(i<0) S.feeds.push(row); else S.feeds[i]=row; + if(row.id===S.feed) patchFeedHead(row); if(!rowsQueued) rowsQueued=requestAnimationFrame(()=>{ rowsQueued=0; renderFeeds(); }); } @@ -200,10 +201,10 @@ function selectFeed(id: string){ /// set (it has been failing for a day and is a kind worth naming -- see `explain_failure` in /// src/feed.rs). Anything else still shows the raw error, as before. function failBannerHTML(f: Feed){ - if(f.failing) return `
${esc(f.failing.reason)} + if(f.failing) return `
${esc(f.failing.reason)} ${ f.failing.new_url?` `:''}
`; - if(f.last_error) return `
${esc(f.last_error)}
`; + if(f.last_error) return `
${esc(f.last_error)}
`; return ''; } function wireFailBanner(box: ParentNode,f: Feed){