Show a failing feed on its artwork, in words, and grey (#93)
The mark was a 12px "!" in the sidebar's margin, told apart by --bad alone; a dark theme's --bad is a pale pink, and at that size it vanished. It is now a solid disc on the artwork's corner, the subtitle says what is wrong in place of the counts, and a feed failing for a day or more has its artwork greyed out. Lightness and words carry it, so no theme's palette changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### 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.
|
- 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`.
|
- 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
|
- A feed whose website names an icon that is missing shows the site's `/favicon.ico` instead of
|
||||||
|
|||||||
@@ -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();
|
await expect(page.locator('.feed.group .chev').first()).toBeVisible();
|
||||||
if ((await page.locator('.feed.group .chev').first().getAttribute('aria-expanded')) !== 'true')
|
if ((await page.locator('.feed.group .chev').first().getAttribute('aria-expanded')) !== 'true')
|
||||||
await page.locator('.feed.group .chev').first().click();
|
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.
|
// Faked in the page: no fixture feed fails. A feed on its own failing for a day, and one
|
||||||
await page.evaluate(() => {
|
// 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).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();
|
renderFeeds();
|
||||||
|
return f.id;
|
||||||
});
|
});
|
||||||
await expect(page.locator('.ferr')).toHaveCount(2);
|
await expect(page.locator('.fart .ferr svg')).toHaveCount(3); // both feeds and the folder
|
||||||
await expect(page.locator('.ferr svg')).toHaveCount(2); // the icon, not a "!"
|
|
||||||
await expect(page.locator('.chev.bad')).toHaveCount(1); // the folder holding one
|
await expect(page.locator('.chev.bad')).toHaveCount(1); // the folder holding one
|
||||||
const xs = await page.$$eval('.chev, .ferr', els =>
|
const row = page.locator(`.feed[data-id="${solo}"]`);
|
||||||
els.map(e => { const r = e.getBoundingClientRect(); return Math.round(r.left + r.width / 2); }));
|
await expect(row).toHaveClass(/failing/);
|
||||||
expect(new Set(xs).size, JSON.stringify(xs)).toBe(1);
|
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
|
await page.reload(); // put the real list back
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
17
web/app.css
17
web/app.css
@@ -588,11 +588,18 @@ input:focus,select:focus{outline:0;border-color:var(--accent)}
|
|||||||
@keyframes ipxspin{to{transform:rotate(360deg)}}
|
@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{
|
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}
|
animation:ipxspin 1s linear infinite}
|
||||||
/* A feed's error mark, in the triangle's place: the same column as every folder's triangle,
|
/* A feed's error mark: a solid disc on the artwork's corner, ringed in the page ground so it
|
||||||
a child's included, which is why it moves left by the child's indent. */
|
stands off any cover. The glyph is cut out of it in --bg, which is the dark against the light
|
||||||
.ferr{position:absolute;left:-16px;top:0;bottom:0;width:24px;display:grid;place-items:center;color:var(--bad)}
|
--bad of a dark theme and the light against the deep one of a light theme. */
|
||||||
.ferr .i{width:12px;height:12px}
|
.fart{position:relative;flex:none;display:grid}
|
||||||
.feed.child .ferr{left:-27px}
|
.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 .i{width:12px;height:12px;transition:transform .12s}
|
||||||
.chev[aria-expanded="true"] .i{transform:rotate(90deg)}
|
.chev[aria-expanded="true"] .i{transform:rotate(90deg)}
|
||||||
.childlist{display:grid;grid-template-columns:minmax(0,1fr);gap:4px;margin-top:10px}
|
.childlist{display:grid;grid-template-columns:minmax(0,1fr);gap:4px;margin-top:10px}
|
||||||
|
|||||||
@@ -105,19 +105,25 @@ function renderFeeds(){
|
|||||||
// went wrong (failBannerHTML); the list only has to make it findable.
|
// went wrong (failBannerHTML); the list only has to make it findable.
|
||||||
const bad=c=>c.failing?.reason||c.last_error;
|
const bad=c=>c.failing?.reason||c.last_error;
|
||||||
const err=mine.length ? mine.map(bad).find(Boolean) : bad(f);
|
const err=mine.length ? mine.map(bad).find(Boolean) : bad(f);
|
||||||
|
const nbad=mine.filter(bad).length;
|
||||||
const el=document.createElement('div');
|
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':'')+
|
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':'');
|
(f.pinned&&!depth?' pinned':'')+(f===lastPin&&lastTop>=0?' lastpin':'');
|
||||||
el.tabIndex=0; el.dataset.id=f.id;
|
el.tabIndex=0; el.dataset.id=f.id;
|
||||||
const open = !!(kids && (expanded.has(f.id) || q));
|
const open = !!(kids && (expanded.has(f.id) || q));
|
||||||
el.innerHTML =
|
el.innerHTML =
|
||||||
// The error mark hangs in the margin where a folder's triangle does. A folder already has
|
(kids?`<button class="chev${err?' bad':''}" aria-expanded="${open}" title="${err?`A feed inside has a problem: ${esc(err)}`:'Show or hide the feeds inside'}" aria-label="Show or hide the feeds inside">${ICON.caret}</button>`:'')+
|
||||||
// its triangle there, so that turns red instead, and the feed inside shows the mark.
|
// The mark sits on the artwork, the thing the eye scans the list by, and the line under
|
||||||
(kids?`<button class="chev${err?' bad':''}" aria-expanded="${open}" title="${err?`A feed inside has a problem: ${esc(err)}`:'Show or hide the feeds inside'}" aria-label="Show or hide the feeds inside">${ICON.caret}</button>`
|
// the name says what is wrong in place of the counts.
|
||||||
:err?`<span class="ferr" role="img" title="${esc(err)}" aria-label="Error: ${esc(err)}">${ICON.alert}</span>`:'')+
|
`<span class="fart">${mine.length?folderArt(f,mine):artHTML(f.image,f.title||f.id)}`+
|
||||||
(mine.length?folderArt(f,mine):artHTML(f.image,f.title||f.id))+
|
(err?`<span class="ferr" role="img" title="${esc(err)}" aria-label="Error: ${esc(err)}">${ICON.alert}</span>`:'')+`</span>`+
|
||||||
`<div class="txt"><b>${f.pinned?`<span class="fpin" title="Pinned">${ICON.pinOn}</span>`:''}${esc(f.title||f.id)}</b><small>`+
|
`<div class="txt"><b>${f.pinned?`<span class="fpin" title="Pinned">${ICON.pinOn}</span>`:''}${esc(f.title||f.id)}</b><small>`+
|
||||||
`${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`)+
|
||||||
`</small></div>`+
|
`</small></div>`+
|
||||||
(f.orphaned?'<span class="tag" title="No longer listed, kept because it has downloads">Gone</span>':'')+
|
(f.orphaned?'<span class="tag" title="No longer listed, kept because it has downloads">Gone</span>':'')+
|
||||||
`<span class="badge${unread?'':' zero'}" title="${unread} unread">${unread>999?'999+':unread}</span>`;
|
`<span class="badge${unread?'':' zero'}" title="${unread} unread">${unread>999?'999+':unread}</span>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user