diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f87e2b..f113e1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A failing feed is easy to spot in any theme: a mark on its artwork, what is wrong in place of + its counts, and its artwork greyed out once it has failed for a day. - A feed whose own artwork is missing shows its website's icon instead of nothing. - The log has no terminal colour codes when it is not going to a terminal, as in `docker logs`. - A feed whose website names an icon that is missing shows the site's `/favicon.ico` instead of diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index a68569c..4d7375a 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1211,22 +1211,26 @@ test('the favicon is the logo, square, from both pages', async ({ page }) => { } }); -test('a feed error is marked in the same column as the folder triangles', async ({ page }) => { +test('a failing feed is marked on its artwork and says why', async ({ page }) => { await expect(page.locator('.feed.group .chev').first()).toBeVisible(); if ((await page.locator('.feed.group .chev').first().getAttribute('aria-expanded')) !== 'true') await page.locator('.feed.group .chev').first().click(); - // Faked in the page: no fixture feed fails. A feed on its own, and one inside a folder. - await page.evaluate(() => { + // Faked in the page: no fixture feed fails. A feed on its own failing for a day, and one + // inside a folder that failed its last check. + const solo = await page.evaluate(() => { S.feeds.find(f => f.group).last_error = 'HTTP 404'; - S.feeds.find(f => !f.group && !S.feeds.some(c => c.group === f.id)).last_error = 'timed out'; + const f = S.feeds.find(f => !f.group && !S.feeds.some(c => c.group === f.id)); + f.last_error = 'HTTP 404 Not Found'; + f.failing = { reason: 'The publisher took this feed down, or moved it.' }; renderFeeds(); + return f.id; }); - await expect(page.locator('.ferr')).toHaveCount(2); - await expect(page.locator('.ferr svg')).toHaveCount(2); // the icon, not a "!" + await expect(page.locator('.fart .ferr svg')).toHaveCount(3); // both feeds and the folder await expect(page.locator('.chev.bad')).toHaveCount(1); // the folder holding one - const xs = await page.$$eval('.chev, .ferr', els => - els.map(e => { const r = e.getBoundingClientRect(); return Math.round(r.left + r.width / 2); })); - expect(new Set(xs).size, JSON.stringify(xs)).toBe(1); + const row = page.locator(`.feed[data-id="${solo}"]`); + await expect(row).toHaveClass(/failing/); + await expect(row.locator('small')).toHaveText('The publisher took this feed down, or moved it.'); + await expect(page.locator('.feed.group.err small').first()).toHaveText('1 feed not updating'); await page.reload(); // put the real list back }); diff --git a/web/app.css b/web/app.css index a3623d5..958516b 100644 --- a/web/app.css +++ b/web/app.css @@ -588,11 +588,18 @@ input:focus,select:focus{outline:0;border-color:var(--accent)} @keyframes ipxspin{to{transform:rotate(360deg)}} body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i,body.scan-any #scanAll .i{ animation:ipxspin 1s linear infinite} -/* A feed's error mark, in the triangle's place: the same column as every folder's triangle, - a child's included, which is why it moves left by the child's indent. */ -.ferr{position:absolute;left:-16px;top:0;bottom:0;width:24px;display:grid;place-items:center;color:var(--bad)} -.ferr .i{width:12px;height:12px} -.feed.child .ferr{left:-27px} +/* A feed's error mark: a solid disc on the artwork's corner, ringed in the page ground so it + stands off any cover. The glyph is cut out of it in --bg, which is the dark against the light + --bad of a dark theme and the light against the deep one of a light theme. */ +.fart{position:relative;flex:none;display:grid} +.ferr{ + position:absolute;right:-5px;bottom:-5px;width:17px;height:17px;border-radius:50%; + display:grid;place-items:center;background:var(--bad);color:var(--bg);border:2px solid var(--bg); +} +.ferr .i{width:9px;height:9px} +.feed.err .txt small{color:var(--bad)} +/* Failing for a day or more: the cover goes grey, a show gone off the air. */ +.feed.failing .fart>.art{filter:grayscale(1);opacity:.45} .chev .i{width:12px;height:12px;transition:transform .12s} .chev[aria-expanded="true"] .i{transform:rotate(90deg)} .childlist{display:grid;grid-template-columns:minmax(0,1fr);gap:4px;margin-top:10px} diff --git a/web/src/feeds.ts b/web/src/feeds.ts index 517c145..197f006 100644 --- a/web/src/feeds.ts +++ b/web/src/feeds.ts @@ -105,19 +105,25 @@ function renderFeeds(){ // went wrong (failBannerHTML); the list only has to make it findable. const bad=c=>c.failing?.reason||c.last_error; const err=mine.length ? mine.map(bad).find(Boolean) : bad(f); + const nbad=mine.filter(bad).length; const el=document.createElement('div'); + // A feed that has failed for a day goes grey, as if switched off: a change in lightness + // shows in every theme, where the red mark alone was easy to miss on a dark one. el.className='feed'+(S.feed===f.id?' sel':'')+(depth?' child':'')+(kids?' group':'')+ + (err?' err':'')+(f.failing?' failing':'')+ (f.pinned&&!depth?' pinned':'')+(f===lastPin&&lastTop>=0?' lastpin':''); el.tabIndex=0; el.dataset.id=f.id; const open = !!(kids && (expanded.has(f.id) || q)); el.innerHTML = - // The error mark hangs in the margin where a folder's triangle does. A folder already has - // its triangle there, so that turns red instead, and the feed inside shows the mark. - (kids?`` - :err?`${ICON.alert}`:'')+ - (mine.length?folderArt(f,mine):artHTML(f.image,f.title||f.id))+ + (kids?``:'')+ + // The mark sits on the artwork, the thing the eye scans the list by, and the line under + // the name says what is wrong in place of the counts. + `${mine.length?folderArt(f,mine):artHTML(f.image,f.title||f.id)}`+ + (err?`${ICON.alert}`:'')+``+ `
${f.pinned?`${ICON.pinOn}`:''}${esc(f.title||f.id)}`+ - `${mine.length?plural(mine.length,'feed'):plural(eps,'item')} · ${saved} downloaded`+ + (nbad?`${plural(nbad,'feed')} not updating` + :err?esc(f.failing?.reason||'The last check failed') + :`${mine.length?plural(mine.length,'feed'):plural(eps,'item')} · ${saved} downloaded`)+ `
`+ (f.orphaned?'Gone':'')+ `${unread>999?'999+':unread}`;