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:
47
src/db.rs
47
src/db.rs
@@ -22,6 +22,10 @@ CREATE TABLE IF NOT EXISTS feeds (
|
||||
last_checked INTEGER,
|
||||
ttl_mins INTEGER,
|
||||
last_error TEXT,
|
||||
-- When the current run of failures began; NULL while the feed is healthy. Kept
|
||||
-- through repeated failures so the UI can tell a blip (macmanx: failed once, fine an
|
||||
-- hour later) from a feed that has been down for a day.
|
||||
error_since INTEGER,
|
||||
-- Came from a subscribed OPML that no longer lists it, but has downloads, so kept.
|
||||
orphaned INTEGER NOT NULL DEFAULT 0,
|
||||
-- The OPML subscription this feed came from.
|
||||
@@ -165,6 +169,7 @@ fn migrate(conn: &Connection) -> Result<()> {
|
||||
// and it came back the same day with `last_login` beside it.
|
||||
("users", "created", "INTEGER"),
|
||||
("users", "last_login", "INTEGER"),
|
||||
("feeds", "error_since", "INTEGER"),
|
||||
];
|
||||
let retired: &[(&str, &str)] = &[
|
||||
// Read state from before accounts, long since moved to entry_state. Two bugs came from
|
||||
@@ -208,6 +213,8 @@ pub struct FeedSummary {
|
||||
pub orphaned: bool,
|
||||
pub last_checked: Option<i64>,
|
||||
pub last_error: Option<String>,
|
||||
/// When this run of failures began; see the `error_since` column.
|
||||
pub error_since: Option<i64>,
|
||||
pub entries: i64,
|
||||
pub downloaded: i64,
|
||||
}
|
||||
@@ -249,7 +256,7 @@ impl Db {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut sum: FeedSummary = conn
|
||||
.query_row(
|
||||
"SELECT title, image, last_checked, last_error, coalesce(orphaned, 0)
|
||||
"SELECT title, image, last_checked, last_error, coalesce(orphaned, 0), error_since
|
||||
FROM feeds WHERE id = ?1",
|
||||
[feed_id],
|
||||
|r| {
|
||||
@@ -259,6 +266,7 @@ impl Db {
|
||||
last_checked: r.get(2)?,
|
||||
last_error: r.get(3)?,
|
||||
orphaned: r.get::<_, i64>(4)? != 0,
|
||||
error_since: r.get(5)?,
|
||||
..Default::default()
|
||||
})
|
||||
},
|
||||
@@ -333,7 +341,8 @@ impl Db {
|
||||
last_checked = excluded.last_checked,
|
||||
ttl_mins = excluded.ttl_mins,
|
||||
image = coalesce(excluded.image, feeds.image),
|
||||
last_error = NULL",
|
||||
last_error = NULL,
|
||||
error_since = NULL",
|
||||
rusqlite::params![feed_id, url, title, etag, last_modified, now(), ttl_mins.map(|t| t as i64), image],
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -344,7 +353,8 @@ impl Db {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO feeds (id, url, last_checked) VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT(id) DO UPDATE SET last_checked = excluded.last_checked, last_error = NULL",
|
||||
ON CONFLICT(id) DO UPDATE SET last_checked = excluded.last_checked,
|
||||
last_error = NULL, error_since = NULL",
|
||||
rusqlite::params![feed_id, url, now()],
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -363,10 +373,15 @@ impl Db {
|
||||
|
||||
pub fn set_feed_error(&self, feed_id: &str, url: &str, msg: &str) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = now();
|
||||
conn.execute(
|
||||
"INSERT INTO feeds (id, url, last_checked, last_error) VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT(id) DO UPDATE SET last_checked = excluded.last_checked, last_error = excluded.last_error",
|
||||
rusqlite::params![feed_id, url, now(), msg],
|
||||
"INSERT INTO feeds (id, url, last_checked, last_error, error_since)
|
||||
VALUES (?1, ?2, ?3, ?4, ?3)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
last_checked = excluded.last_checked,
|
||||
last_error = excluded.last_error,
|
||||
error_since = coalesce(feeds.error_since, excluded.error_since)",
|
||||
rusqlite::params![feed_id, url, now, msg],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1766,6 +1781,26 @@ mod tests {
|
||||
assert_eq!(explicit(None), [None, Some(false)], "outside a group nothing is inherited");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_since_marks_the_start_of_a_run_of_failures_and_clears_on_success() {
|
||||
let db = Db::memory().unwrap();
|
||||
db.set_feed_error("f", "http://x", "HTTP 404").unwrap();
|
||||
// Backdate it, as if this feed had already been failing a while, so a second
|
||||
// failure landing "now" is distinguishable from the first.
|
||||
db.exec_for_test("UPDATE feeds SET error_since = error_since - 3600 WHERE id = 'f'").unwrap();
|
||||
let first = db.feed_summary("f").unwrap().error_since.unwrap();
|
||||
|
||||
// macmanx: failed once, read fine an hour later. A second failure must not push
|
||||
// error_since forward -- the UI decides "failing for a day" from the first one.
|
||||
db.set_feed_error("f", "http://x", "HTTP 404").unwrap();
|
||||
assert_eq!(db.feed_summary("f").unwrap().error_since, Some(first));
|
||||
|
||||
db.touch_feed("f", "http://x").unwrap();
|
||||
let after = db.feed_summary("f").unwrap();
|
||||
assert_eq!(after.last_error, None);
|
||||
assert_eq!(after.error_since, None, "a clean check ends the run of failures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enclosure_url_is_the_dedupe_key() {
|
||||
let db = Db::memory().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user