Release 0.5.3: OPML orphan scan, feed error UI, small UI fixes
- Stop scanning an OPML/Patreon feed's derived rows once nobody subscribes to it; retire them (drop or orphan) the way sync_group already does when the list itself drops one. This is what let 922 defunct davewiner feeds keep scanning hourly after the OPML left config. - Repair feed XML with a bare `&`, and give a plain reason (moved web page with its new address when linked, or nothing yet for an empty body) instead of a raw parser error. - Show a failing feed's plain-English reason and next step (Unsubscribe / Use the new address) in the sidebar and on its own page, once it has been down a day. - Fix four small UI bugs: show-note links open in a new tab, video files play as video, an opened item no longer disappears from the Unread tab, and Subscribe/Unsubscribe get their own icons. - Fix Settings disappearing for non-admin accounts: it was hiding the whole modal instead of just the admin-only parts (Users, the editable schedule/quota, Save), which are the only parts the server actually refuses them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DmQfE1eFPApnXWyPHBWqUA
This commit is contained in:
25
src/web.rs
25
src/web.rs
@@ -488,6 +488,9 @@ struct FeedRow {
|
||||
last_checked: Option<i64>,
|
||||
next_check: Option<i64>,
|
||||
last_error: Option<String>,
|
||||
/// Set once `last_error` is a kind worth telling someone about and it has held for a
|
||||
/// day -- a feed that fails once and reads fine an hour later (macmanx) never gets here.
|
||||
failing: Option<FailingRow>,
|
||||
entries: i64,
|
||||
downloaded: i64,
|
||||
unread: i64,
|
||||
@@ -495,6 +498,15 @@ struct FeedRow {
|
||||
subscribers: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FailingRow {
|
||||
reason: &'static str,
|
||||
new_url: Option<String>,
|
||||
}
|
||||
|
||||
/// A day, in seconds: how long an error has to hold before the UI mentions it.
|
||||
const FLAG_AFTER_SECS: i64 = 86_400;
|
||||
|
||||
async fn feeds(
|
||||
State(state): State<WebState>,
|
||||
user: crate::db::User,
|
||||
@@ -558,6 +570,12 @@ async fn feeds(
|
||||
next_check: s
|
||||
.last_checked
|
||||
.map(|t| t + crate::due_after(&cfg, feed, st.ttl_mins) as i64),
|
||||
failing: s
|
||||
.error_since
|
||||
.filter(|since| crate::db::now() - since >= FLAG_AFTER_SECS)
|
||||
.and_then(|_| s.last_error.as_deref())
|
||||
.and_then(crate::feed::explain_failure)
|
||||
.map(|f| FailingRow { reason: f.reason, new_url: f.new_url }),
|
||||
last_error: s.last_error,
|
||||
entries: s.entries,
|
||||
downloaded: s.downloaded,
|
||||
@@ -917,9 +935,13 @@ fn entry_page(
|
||||
let mut rows =
|
||||
db.entries_in(user_id, feed, filter, search, page.offset, page.limit.clamp(1, 200), &order)?;
|
||||
// Feed HTML is untrusted: it reaches the page only after ammonia has been through it.
|
||||
// Every link opens in a new tab -- ammonia's default rel="noopener noreferrer" already
|
||||
// keeps that safe -- so following one in show notes never navigates away from ipx.
|
||||
let mut sanitizer = ammonia::Builder::new();
|
||||
sanitizer.add_tag_attributes("a", &["target"]).set_tag_attribute_value("a", "target", "_blank");
|
||||
for row in &mut rows {
|
||||
if let Some(d) = &row.description {
|
||||
row.description = Some(ammonia::clean(d));
|
||||
row.description = Some(sanitizer.clean(d).to_string());
|
||||
}
|
||||
}
|
||||
let total = db.count_in(user_id, feed, filter, search)?;
|
||||
@@ -1147,6 +1169,7 @@ async fn remove_feed(
|
||||
}
|
||||
cfg.save(&state.config_path)?;
|
||||
state.ctx.reload_cfg(&state.config_path)?;
|
||||
crate::retire_group(&state.ctx, &id)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user