diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a83d5..4cdc407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - A publisher's correction to an item already in iPX, a retitled episode, mended text, new artwork or a fixed length, arrives on the next read. Since 0.9.1 an item kept what it first said: Ain't It Cool News's items each showed the text of the one before, long after the feed was fixed. +- A feed's page in the Directory says when the feed is failing, and why, and no longer says nobody here subscribes to a feed someone does. ## [0.10.1] - 2026-10-05 diff --git a/src/web.rs b/src/web.rs index 41c3dd1..43ec937 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1024,9 +1024,17 @@ struct ListedItem { struct Listed { /// What the feed says it is (#130), sanitized as its items are. description: Option, + /// Why its last check failed, when it did (#142), in the words a feed's own page uses. + failing: Option, items: Vec, } +/// A feed's last error as its Directory page says it: explained, or that the check failed, never +/// the error as it stands, which can name the feed's address, and the Directory names none. +fn failure_words(err: &str) -> String { + crate::feed::explain_failure(err).map_or_else(|| "Its last check failed.".to_owned(), |f| f.reason.to_owned()) +} + /// A listed feed's description and latest twenty items. Only a feed the Directory lists, so a /// guessed id reaches nothing private, as `subscribe_popular` checks. async fn get_listed( @@ -1041,6 +1049,7 @@ async fn get_listed( let rows = state.ctx.db.entries_in(user.id, Some(&id), crate::db::Filter::parse("all"), None, 0, 20, &order).await?; let mut sanitizer = feed_sanitizer(); let description = state.ctx.db.feed_description(&id).await?.map(|d| clean_description(&mut sanitizer, &d, None)); + let failing = state.ctx.db.feed_summary(&id).await?.last_error.as_deref().map(failure_words); let items = rows .into_iter() .map(|e| ListedItem { @@ -1052,7 +1061,7 @@ async fn get_listed( duration: e.duration, }) .collect(); - Ok(Json(Listed { description, items })) + Ok(Json(Listed { description, failing, items })) } fn sort_name(p: &PopularRow) -> String { @@ -1146,6 +1155,14 @@ impl IntoResponse for ApiError { mod tests { use super::*; + #[test] + fn a_failing_feed_says_why_in_the_directory_without_its_address() { + assert_eq!(failure_words("the feed's address answered with nothing at all"), "This address answers with nothing; the site may be gone."); + // reqwest names the URL; the Directory names none. + let raw = "error sending request for url (http://daily-quests.com/comic/?feed=atom): connection closed"; + assert_eq!(failure_words(raw), "Its last check failed."); + } + #[test] fn a_relative_image_resolves_against_the_post() { let mut b = feed_sanitizer(); diff --git a/web/app.css b/web/app.css index 1364522..0b504f3 100644 --- a/web/app.css +++ b/web/app.css @@ -717,6 +717,7 @@ body.scan-this .fhead [data-a=scan] .i,body.scan-any .fhead [data-a=scanall] .i, .show .meta{min-width:0;flex:1;display:flex;flex-direction:column;align-items:flex-start;gap:5px} .show h2{margin:0 0 2px;font-size:30px;line-height:1.15;font-weight:700;letter-spacing:-.02em;overflow-wrap:anywhere} .show .sub{margin:0;color:var(--dim);font-size:13px;font-variant-numeric:tabular-nums} +.show .dfail{color:var(--bad)} .show .acts{margin-top:8px} .dlink{padding:0;font-size:13px;color:var(--accent);text-align:left} .dlink:hover{text-decoration:underline} diff --git a/web/src/dialogs.ts b/web/src/dialogs.ts index 2dc5bed..449291f 100644 --- a/web/src/dialogs.ts +++ b/web/src/dialogs.ts @@ -138,6 +138,7 @@ function drawDirectory(box=$('#directory'), focus?: string){ // Its category's page is one click away from here. show.category?``:''}

${n?`${n} ${n===1?'person here subscribes':'people here subscribe'}`:'Nobody here subscribes yet'}

+
` +sec(show.podcast?'Latest episodes':'Latest posts','

Loading…

'); @@ -217,6 +218,8 @@ async function fillShow(box,p){ more.textContent=open?'Less':'More'; more.setAttribute('aria-expanded',String(open)); }; } + // A broken feed says so, or it reads as a quiet one (#142). + if(got.failing){ const f=$('#dfail',box); f.textContent=got.failing; f.hidden=false; } const items=got.items; list.innerHTML=items.length?items.map(e=>{ const name=entryName(e), text=name.derived?'':plainText(e.description).slice(0,500); @@ -224,7 +227,8 @@ async function fillShow(box,p){ return `
${e.link?`${esc(name.text)}`:esc(name.text)}` +(text?`

${esc(text)}

`:'') +(when.length?`${when.map(s=>`${s}`).join('')}`:'')+'
'; - }).join(''):`

Nothing read from it yet. A feed nobody here subscribes to is checked once a day.

`; + }).join(''):`

Nothing read from it yet.${p.subscribers||got.failing?'' + :' A feed nobody here subscribes to is checked once a day.'}

`; } /// A category as a tile: its name, how many feeds, and the covers of its three most subscribed