max / balanced_breakfast
23 files changed,
+422 insertions,
-276 deletions
| @@ -79,20 +79,7 @@ Config field types: `Text`, `TextArea`, `Secret`, `Url`, `Number`, `Toggle`, `Se | |||
| 79 | 79 | ||
| 80 | 80 | ### Sandbox Limits | |
| 81 | 81 | ||
| 82 | - | Each plugin runs in an isolated Rhai engine with strict limits: | |
| 83 | - | ||
| 84 | - | | Limit | Value | Purpose | | |
| 85 | - | |-------|-------|---------| | |
| 86 | - | | Max operations | 100,000 | Catches infinite loops | | |
| 87 | - | | Max expression depth | 128 | Prevents stack overflow | | |
| 88 | - | | Max recursion | 32 | Limits call depth | | |
| 89 | - | | HTTP timeout | 15s per request | Prevents hanging | | |
| 90 | - | | Response size | 2 MB per response | Prevents memory exhaustion | | |
| 91 | - | | Max requests | 100 per fetch | Catches runaway fetchers | | |
| 92 | - | | Aggregate timeout | 60s per fetch | Hard ceiling on total fetch time | | |
| 93 | - | | URL restrictions | Block localhost, private IPs | Prevents SSRF | | |
| 94 | - | ||
| 95 | - | Each plugin gets its own engine instance with isolated counters (request count via `Arc<AtomicUsize>`, deadline via `Arc<AtomicU64>`). | |
| 82 | + | Each plugin runs in an isolated Rhai engine with strict limits (operations, recursion, HTTP timeout/count, response size, SSRF blocking). Full limit table and isolation details in `docs/architecture.md` § Sandboxing. | |
| 96 | 83 | ||
| 97 | 84 | ### Plugin Style | |
| 98 | 85 |
| @@ -3693,6 +3693,16 @@ dependencies = [ | |||
| 3693 | 3693 | ] | |
| 3694 | 3694 | ||
| 3695 | 3695 | [[package]] | |
| 3696 | + | name = "rand" | |
| 3697 | + | version = "0.9.4" | |
| 3698 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3699 | + | checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" | |
| 3700 | + | dependencies = [ | |
| 3701 | + | "rand_chacha 0.9.0", | |
| 3702 | + | "rand_core 0.9.5", | |
| 3703 | + | ] | |
| 3704 | + | ||
| 3705 | + | [[package]] | |
| 3696 | 3706 | name = "rand_chacha" | |
| 3697 | 3707 | version = "0.2.2" | |
| 3698 | 3708 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3713,6 +3723,16 @@ dependencies = [ | |||
| 3713 | 3723 | ] | |
| 3714 | 3724 | ||
| 3715 | 3725 | [[package]] | |
| 3726 | + | name = "rand_chacha" | |
| 3727 | + | version = "0.9.0" | |
| 3728 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3729 | + | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" | |
| 3730 | + | dependencies = [ | |
| 3731 | + | "ppv-lite86", | |
| 3732 | + | "rand_core 0.9.5", | |
| 3733 | + | ] | |
| 3734 | + | ||
| 3735 | + | [[package]] | |
| 3716 | 3736 | name = "rand_core" | |
| 3717 | 3737 | version = "0.5.1" | |
| 3718 | 3738 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3731,6 +3751,15 @@ dependencies = [ | |||
| 3731 | 3751 | ] | |
| 3732 | 3752 | ||
| 3733 | 3753 | [[package]] | |
| 3754 | + | name = "rand_core" | |
| 3755 | + | version = "0.9.5" | |
| 3756 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3757 | + | checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" | |
| 3758 | + | dependencies = [ | |
| 3759 | + | "getrandom 0.3.4", | |
| 3760 | + | ] | |
| 3761 | + | ||
| 3762 | + | [[package]] | |
| 3734 | 3763 | name = "rand_hc" | |
| 3735 | 3764 | version = "0.2.0" | |
| 3736 | 3765 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -5052,7 +5081,7 @@ dependencies = [ | |||
| 5052 | 5081 | "chrono", | |
| 5053 | 5082 | "keyring", | |
| 5054 | 5083 | "parking_lot", | |
| 5055 | - | "rand 0.8.5", | |
| 5084 | + | "rand 0.9.4", | |
| 5056 | 5085 | "reqwest 0.12.28", | |
| 5057 | 5086 | "serde", | |
| 5058 | 5087 | "serde_json", |
| @@ -11,7 +11,7 @@ default-members = ["src-tauri"] | |||
| 11 | 11 | ||
| 12 | 12 | [workspace.package] | |
| 13 | 13 | version = "0.3.1" | |
| 14 | - | edition = "2021" | |
| 14 | + | edition = "2024" | |
| 15 | 15 | authors = ["BalancedBreakfast Contributors"] | |
| 16 | 16 | license-file = "LICENSE" | |
| 17 | 17 |
| @@ -653,6 +653,32 @@ impl ItemsRepository { | |||
| 653 | 653 | Ok(()) | |
| 654 | 654 | } | |
| 655 | 655 | ||
| 656 | + | /// Mark all unread items as read, optionally filtered to a specific source. | |
| 657 | + | #[tracing::instrument(skip_all)] | |
| 658 | + | pub async fn mark_all_read(&self, busser_id: Option<&str>) -> Result<u64, sqlx::Error> { | |
| 659 | + | let now = Utc::now().format(TIMESTAMP_FMT).to_string(); | |
| 660 | + | let result = match busser_id { | |
| 661 | + | Some(id) => { | |
| 662 | + | sqlx::query( | |
| 663 | + | "UPDATE feed_items SET is_read = 1, updated_at = ?1 WHERE is_read = 0 AND busser_id = ?2", | |
| 664 | + | ) | |
| 665 | + | .bind(&now) | |
| 666 | + | .bind(id) | |
| 667 | + | .execute(&self.pool) | |
| 668 | + | .await? | |
| 669 | + | } | |
| 670 | + | None => { | |
| 671 | + | sqlx::query( | |
| 672 | + | "UPDATE feed_items SET is_read = 1, updated_at = ?1 WHERE is_read = 0", | |
| 673 | + | ) | |
| 674 | + | .bind(&now) | |
| 675 | + | .execute(&self.pool) | |
| 676 | + | .await? | |
| 677 | + | } | |
| 678 | + | }; | |
| 679 | + | Ok(result.rows_affected()) | |
| 680 | + | } | |
| 681 | + | ||
| 656 | 682 | /// Count all feed items. | |
| 657 | 683 | #[tracing::instrument(skip_all)] | |
| 658 | 684 | pub async fn count_all(&self) -> Result<i64, sqlx::Error> { |
| @@ -43,8 +43,8 @@ mod tests { | |||
| 43 | 43 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 44 | 44 | seed_item(&db, &feed, "rss:mr", 0).await; | |
| 45 | 45 | ||
| 46 | - | let gen = FeedGenerator::new(db.clone()); | |
| 47 | - | gen.mark_read("rss:mr", true).await.unwrap(); | |
| 46 | + | let fg = FeedGenerator::new(db.clone()); | |
| 47 | + | fg.mark_read("rss:mr", true).await.unwrap(); | |
| 48 | 48 | ||
| 49 | 49 | let item = db.items().get_by_external_id("rss:mr").await.unwrap().unwrap(); | |
| 50 | 50 | assert!(item.is_read); | |
| @@ -56,8 +56,8 @@ mod tests { | |||
| 56 | 56 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 57 | 57 | seed_item(&db, &feed, "rss:ms", 0).await; | |
| 58 | 58 | ||
| 59 | - | let gen = FeedGenerator::new(db.clone()); | |
| 60 | - | gen.mark_starred("rss:ms", true).await.unwrap(); | |
| 59 | + | let fg = FeedGenerator::new(db.clone()); | |
| 60 | + | fg.mark_starred("rss:ms", true).await.unwrap(); | |
| 61 | 61 | ||
| 62 | 62 | let item = db.items().get_by_external_id("rss:ms").await.unwrap().unwrap(); | |
| 63 | 63 | assert!(item.is_starred); | |
| @@ -66,9 +66,9 @@ mod tests { | |||
| 66 | 66 | #[tokio::test] | |
| 67 | 67 | async fn mark_read_nonexistent_is_noop() { | |
| 68 | 68 | let db = test_db().await; | |
| 69 | - | let gen = FeedGenerator::new(db); | |
| 69 | + | let fg = FeedGenerator::new(db); | |
| 70 | 70 | // Should not error on missing item | |
| 71 | - | gen.mark_read("nonexistent:1", true).await.unwrap(); | |
| 71 | + | fg.mark_read("nonexistent:1", true).await.unwrap(); | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | 74 | // ── mark_starred edge case ────────────────────────────────── | |
| @@ -76,8 +76,8 @@ mod tests { | |||
| 76 | 76 | #[tokio::test] | |
| 77 | 77 | async fn mark_starred_nonexistent_is_noop() { | |
| 78 | 78 | let db = test_db().await; | |
| 79 | - | let gen = FeedGenerator::new(db); | |
| 80 | - | gen.mark_starred("nonexistent:1", true).await.unwrap(); | |
| 79 | + | let fg = FeedGenerator::new(db); | |
| 80 | + | fg.mark_starred("nonexistent:1", true).await.unwrap(); | |
| 81 | 81 | } | |
| 82 | 82 | ||
| 83 | 83 | #[tokio::test] | |
| @@ -86,12 +86,12 @@ mod tests { | |||
| 86 | 86 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 87 | 87 | seed_item(&db, &feed, "rss:toggle", 0).await; | |
| 88 | 88 | ||
| 89 | - | let gen = FeedGenerator::new(db.clone()); | |
| 90 | - | gen.mark_read("rss:toggle", true).await.unwrap(); | |
| 89 | + | let fg = FeedGenerator::new(db.clone()); | |
| 90 | + | fg.mark_read("rss:toggle", true).await.unwrap(); | |
| 91 | 91 | let item = db.items().get_by_external_id("rss:toggle").await.unwrap().unwrap(); | |
| 92 | 92 | assert!(item.is_read); | |
| 93 | 93 | ||
| 94 | - | gen.mark_read("rss:toggle", false).await.unwrap(); | |
| 94 | + | fg.mark_read("rss:toggle", false).await.unwrap(); | |
| 95 | 95 | let item = db.items().get_by_external_id("rss:toggle").await.unwrap().unwrap(); | |
| 96 | 96 | assert!(!item.is_read); | |
| 97 | 97 | } | |
| @@ -102,12 +102,12 @@ mod tests { | |||
| 102 | 102 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 103 | 103 | seed_item(&db, &feed, "rss:toggle", 0).await; | |
| 104 | 104 | ||
| 105 | - | let gen = FeedGenerator::new(db.clone()); | |
| 106 | - | gen.mark_starred("rss:toggle", true).await.unwrap(); | |
| 105 | + | let fg = FeedGenerator::new(db.clone()); | |
| 106 | + | fg.mark_starred("rss:toggle", true).await.unwrap(); | |
| 107 | 107 | let item = db.items().get_by_external_id("rss:toggle").await.unwrap().unwrap(); | |
| 108 | 108 | assert!(item.is_starred); | |
| 109 | 109 | ||
| 110 | - | gen.mark_starred("rss:toggle", false).await.unwrap(); | |
| 110 | + | fg.mark_starred("rss:toggle", false).await.unwrap(); | |
| 111 | 111 | let item = db.items().get_by_external_id("rss:toggle").await.unwrap().unwrap(); | |
| 112 | 112 | assert!(!item.is_starred); | |
| 113 | 113 | } |
| @@ -227,41 +227,41 @@ mod tests { | |||
| 227 | 227 | #[tokio::test] | |
| 228 | 228 | async fn new_has_default_order_and_filter() { | |
| 229 | 229 | let db = test_db().await; | |
| 230 | - | let gen = FeedGenerator::new(db); | |
| 231 | - | assert_eq!(gen.order(), OrderBy::default()); | |
| 232 | - | assert_eq!(gen.filter().source, None); | |
| 233 | - | assert!(!gen.filter().unread_only); | |
| 234 | - | assert!(!gen.filter().starred_only); | |
| 230 | + | let fg = FeedGenerator::new(db); | |
| 231 | + | assert_eq!(fg.order(), OrderBy::default()); | |
| 232 | + | assert_eq!(fg.filter().source, None); | |
| 233 | + | assert!(!fg.filter().unread_only); | |
| 234 | + | assert!(!fg.filter().starred_only); | |
| 235 | 235 | } | |
| 236 | 236 | ||
| 237 | 237 | #[tokio::test] | |
| 238 | 238 | async fn with_order_changes_order() { | |
| 239 | 239 | let db = test_db().await; | |
| 240 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::Score); | |
| 241 | - | assert_eq!(gen.order(), OrderBy::Score); | |
| 240 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::Score); | |
| 241 | + | assert_eq!(fg.order(), OrderBy::Score); | |
| 242 | 242 | } | |
| 243 | 243 | ||
| 244 | 244 | #[tokio::test] | |
| 245 | 245 | async fn with_filter_changes_filter() { | |
| 246 | 246 | let db = test_db().await; | |
| 247 | 247 | let filter = FeedFilter::new().source("rss"); | |
| 248 | - | let gen = FeedGenerator::new(db).with_filter(filter); | |
| 249 | - | assert_eq!(gen.filter().source.as_deref(), Some("rss")); | |
| 248 | + | let fg = FeedGenerator::new(db).with_filter(filter); | |
| 249 | + | assert_eq!(fg.filter().source.as_deref(), Some("rss")); | |
| 250 | 250 | } | |
| 251 | 251 | ||
| 252 | 252 | #[tokio::test] | |
| 253 | 253 | async fn set_order_mutates() { | |
| 254 | 254 | let db = test_db().await; | |
| 255 | - | let mut gen = FeedGenerator::new(db); | |
| 256 | - | gen.set_order(OrderBy::Score); | |
| 257 | - | assert_eq!(gen.order(), OrderBy::Score); | |
| 255 | + | let mut fg = FeedGenerator::new(db); | |
| 256 | + | fg.set_order(OrderBy::Score); | |
| 257 | + | assert_eq!(fg.order(), OrderBy::Score); | |
| 258 | 258 | } | |
| 259 | 259 | ||
| 260 | 260 | #[tokio::test] | |
| 261 | 261 | async fn set_filter_mutates() { | |
| 262 | 262 | let db = test_db().await; | |
| 263 | - | let mut gen = FeedGenerator::new(db); | |
| 264 | - | gen.set_filter(FeedFilter::new().unread_only()); | |
| 265 | - | assert!(gen.filter().unread_only); | |
| 263 | + | let mut fg = FeedGenerator::new(db); | |
| 264 | + | fg.set_filter(FeedFilter::new().unread_only()); | |
| 265 | + | assert!(fg.filter().unread_only); | |
| 266 | 266 | } | |
| 267 | 267 | } |
| @@ -242,8 +242,8 @@ mod tests { | |||
| 242 | 242 | #[tokio::test] | |
| 243 | 243 | async fn get_items_empty_db_returns_empty() { | |
| 244 | 244 | let db = test_db().await; | |
| 245 | - | let gen = FeedGenerator::new(db); | |
| 246 | - | let result = gen.get_items(0).await.unwrap(); | |
| 245 | + | let fg = FeedGenerator::new(db); | |
| 246 | + | let result = fg.get_items(0).await.unwrap(); | |
| 247 | 247 | assert!(result.items.is_empty()); | |
| 248 | 248 | assert!(!result.has_more); | |
| 249 | 249 | } | |
| @@ -255,8 +255,8 @@ mod tests { | |||
| 255 | 255 | seed_item(&db, &feed, "rss:1", 2).await; | |
| 256 | 256 | seed_item(&db, &feed, "rss:2", 1).await; | |
| 257 | 257 | ||
| 258 | - | let gen = FeedGenerator::new(db); | |
| 259 | - | let result = gen.get_items(0).await.unwrap(); | |
| 258 | + | let fg = FeedGenerator::new(db); | |
| 259 | + | let result = fg.get_items(0).await.unwrap(); | |
| 260 | 260 | assert_eq!(result.items.len(), 2); | |
| 261 | 261 | assert!(!result.has_more); | |
| 262 | 262 | } | |
| @@ -269,8 +269,8 @@ mod tests { | |||
| 269 | 269 | seed_item(&db, &feed, &format!("rss:{i}"), i).await; | |
| 270 | 270 | } | |
| 271 | 271 | ||
| 272 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 273 | - | let result = gen.get_items(0).await.unwrap(); | |
| 272 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 273 | + | let result = fg.get_items(0).await.unwrap(); | |
| 274 | 274 | assert_eq!(result.items.len(), 3); | |
| 275 | 275 | assert!(result.has_more); | |
| 276 | 276 | } | |
| @@ -285,8 +285,8 @@ mod tests { | |||
| 285 | 285 | seed_item(&db, &feed, "rss:2", 0).await; | |
| 286 | 286 | seed_item(&db, &feed, "rss:3", 0).await; | |
| 287 | 287 | ||
| 288 | - | let gen = FeedGenerator::new(db); | |
| 289 | - | assert_eq!(gen.count().await.unwrap(), 3); | |
| 288 | + | let fg = FeedGenerator::new(db); | |
| 289 | + | assert_eq!(fg.count().await.unwrap(), 3); | |
| 290 | 290 | } | |
| 291 | 291 | ||
| 292 | 292 | #[tokio::test] | |
| @@ -298,8 +298,8 @@ mod tests { | |||
| 298 | 298 | seed_item(&db, &feed_b, "hn:1", 0).await; | |
| 299 | 299 | seed_item(&db, &feed_b, "hn:2", 0).await; | |
| 300 | 300 | ||
| 301 | - | let gen = FeedGenerator::new(db).with_filter(FeedFilter::new().source("hn")); | |
| 302 | - | assert_eq!(gen.count().await.unwrap(), 2); | |
| 301 | + | let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().source("hn")); | |
| 302 | + | assert_eq!(fg.count().await.unwrap(), 2); | |
| 303 | 303 | } | |
| 304 | 304 | ||
| 305 | 305 | // ── get_sources ────────────────────────────────────────────── | |
| @@ -313,8 +313,8 @@ mod tests { | |||
| 313 | 313 | seed_item(&db, &feed_b, "hn:1", 0).await; | |
| 314 | 314 | seed_item(&db, &feed_b, "hn:2", 0).await; | |
| 315 | 315 | ||
| 316 | - | let gen = FeedGenerator::new(db); | |
| 317 | - | let sources = gen.get_sources().await.unwrap(); | |
| 316 | + | let fg = FeedGenerator::new(db); | |
| 317 | + | let sources = fg.get_sources().await.unwrap(); | |
| 318 | 318 | assert_eq!(sources.len(), 2); | |
| 319 | 319 | ||
| 320 | 320 | let hn_source = sources.iter().find(|s| s.id == "hn").unwrap(); | |
| @@ -332,8 +332,8 @@ mod tests { | |||
| 332 | 332 | seed_item(&db, &feed, &format!("rss:{i}"), i).await; | |
| 333 | 333 | } | |
| 334 | 334 | ||
| 335 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 336 | - | let result = gen.get_items(0).await.unwrap(); | |
| 335 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 336 | + | let result = fg.get_items(0).await.unwrap(); | |
| 337 | 337 | assert_eq!(result.items.len(), 3); | |
| 338 | 338 | assert!(!result.has_more); | |
| 339 | 339 | } | |
| @@ -347,8 +347,8 @@ mod tests { | |||
| 347 | 347 | seed_item(&db, &feed, &format!("rss:{i}"), i).await; | |
| 348 | 348 | } | |
| 349 | 349 | ||
| 350 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 351 | - | let result = gen.get_items(0).await.unwrap(); | |
| 350 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 351 | + | let result = fg.get_items(0).await.unwrap(); | |
| 352 | 352 | assert_eq!(result.items.len(), 3); | |
| 353 | 353 | assert!(result.has_more); | |
| 354 | 354 | } | |
| @@ -361,8 +361,8 @@ mod tests { | |||
| 361 | 361 | seed_item(&db, &feed, &format!("rss:{i}"), i).await; | |
| 362 | 362 | } | |
| 363 | 363 | ||
| 364 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 365 | - | let page1 = gen.get_items(1).await.unwrap(); | |
| 364 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 365 | + | let page1 = fg.get_items(1).await.unwrap(); | |
| 366 | 366 | assert_eq!(page1.items.len(), 2); | |
| 367 | 367 | assert!(!page1.has_more); | |
| 368 | 368 | } | |
| @@ -373,8 +373,8 @@ mod tests { | |||
| 373 | 373 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 374 | 374 | seed_item(&db, &feed, "rss:1", 0).await; | |
| 375 | 375 | ||
| 376 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 377 | - | let result = gen.get_items(5).await.unwrap(); | |
| 376 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 377 | + | let result = fg.get_items(5).await.unwrap(); | |
| 378 | 378 | assert!(result.items.is_empty()); | |
| 379 | 379 | assert!(!result.has_more); | |
| 380 | 380 | } | |
| @@ -395,9 +395,9 @@ mod tests { | |||
| 395 | 395 | .await | |
| 396 | 396 | .unwrap(); | |
| 397 | 397 | ||
| 398 | - | let gen = FeedGenerator::new(db) | |
| 398 | + | let fg = FeedGenerator::new(db) | |
| 399 | 399 | .with_filter(FeedFilter::new().with_feed_tag("tech")); | |
| 400 | - | let result = gen.get_items(0).await.unwrap(); | |
| 400 | + | let result = fg.get_items(0).await.unwrap(); | |
| 401 | 401 | assert_eq!(result.items.len(), 1); | |
| 402 | 402 | assert_eq!(result.items[0].id.source, "rss"); | |
| 403 | 403 | } | |
| @@ -408,9 +408,9 @@ mod tests { | |||
| 408 | 408 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 409 | 409 | seed_item(&db, &feed, "rss:1", 0).await; | |
| 410 | 410 | ||
| 411 | - | let gen = FeedGenerator::new(db) | |
| 411 | + | let fg = FeedGenerator::new(db) | |
| 412 | 412 | .with_filter(FeedFilter::new().with_feed_tag("nonexistent")); | |
| 413 | - | let result = gen.get_items(0).await.unwrap(); | |
| 413 | + | let result = fg.get_items(0).await.unwrap(); | |
| 414 | 414 | assert!(result.items.is_empty()); | |
| 415 | 415 | } | |
| 416 | 416 | ||
| @@ -434,9 +434,9 @@ mod tests { | |||
| 434 | 434 | .await | |
| 435 | 435 | .unwrap(); | |
| 436 | 436 | ||
| 437 | - | let gen = FeedGenerator::new(db) | |
| 437 | + | let fg = FeedGenerator::new(db) | |
| 438 | 438 | .with_filter(FeedFilter::new().with_feed_tag("tech")); | |
| 439 | - | let result = gen.get_items(0).await.unwrap(); | |
| 439 | + | let result = fg.get_items(0).await.unwrap(); | |
| 440 | 440 | assert_eq!(result.items.len(), 2); | |
| 441 | 441 | } | |
| 442 | 442 | ||
| @@ -451,9 +451,9 @@ mod tests { | |||
| 451 | 451 | ||
| 452 | 452 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 453 | 453 | ||
| 454 | - | let gen = FeedGenerator::new(db) | |
| 454 | + | let fg = FeedGenerator::new(db) | |
| 455 | 455 | .with_filter(FeedFilter::new().unread_only()); | |
| 456 | - | let result = gen.get_items(0).await.unwrap(); | |
| 456 | + | let result = fg.get_items(0).await.unwrap(); | |
| 457 | 457 | assert_eq!(result.items.len(), 1); | |
| 458 | 458 | } | |
| 459 | 459 | ||
| @@ -466,9 +466,9 @@ mod tests { | |||
| 466 | 466 | ||
| 467 | 467 | db.items().mark_starred(item2.id, true).await.unwrap(); | |
| 468 | 468 | ||
| 469 | - | let gen = FeedGenerator::new(db) | |
| 469 | + | let fg = FeedGenerator::new(db) | |
| 470 | 470 | .with_filter(FeedFilter::new().starred_only()); | |
| 471 | - | let result = gen.get_items(0).await.unwrap(); | |
| 471 | + | let result = fg.get_items(0).await.unwrap(); | |
| 472 | 472 | assert_eq!(result.items.len(), 1); | |
| 473 | 473 | assert!(result.items[0].is_starred); | |
| 474 | 474 | } | |
| @@ -483,8 +483,8 @@ mod tests { | |||
| 483 | 483 | seed_item(&db, &feed, &format!("rss:{i}"), i).await; | |
| 484 | 484 | } | |
| 485 | 485 | ||
| 486 | - | let gen = FeedGenerator::new(db).with_page_size(2); // page_size irrelevant for get_all | |
| 487 | - | let result = gen.get_all_items().await.unwrap(); | |
| 486 | + | let fg = FeedGenerator::new(db).with_page_size(2); // page_size irrelevant for get_all | |
| 487 | + | let result = fg.get_all_items().await.unwrap(); | |
| 488 | 488 | assert_eq!(result.items.len(), 5); | |
| 489 | 489 | assert!(!result.has_more); | |
| 490 | 490 | } | |
| @@ -497,9 +497,9 @@ mod tests { | |||
| 497 | 497 | seed_item(&db, &feed_a, "rss:1", 0).await; | |
| 498 | 498 | seed_item(&db, &feed_b, "hn:1", 0).await; | |
| 499 | 499 | ||
| 500 | - | let gen = FeedGenerator::new(db) | |
| 500 | + | let fg = FeedGenerator::new(db) | |
| 501 | 501 | .with_filter(FeedFilter::new().source("rss")); | |
| 502 | - | let result = gen.get_all_items().await.unwrap(); | |
| 502 | + | let result = fg.get_all_items().await.unwrap(); | |
| 503 | 503 | assert_eq!(result.items.len(), 1); | |
| 504 | 504 | assert_eq!(result.items[0].id.source, "rss"); | |
| 505 | 505 | } | |
| @@ -511,8 +511,8 @@ mod tests { | |||
| 511 | 511 | seed_item(&db, &feed, "rss:old", 10).await; | |
| 512 | 512 | seed_item(&db, &feed, "rss:new", 1).await; | |
| 513 | 513 | ||
| 514 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::Chronological); | |
| 515 | - | let result = gen.get_all_items().await.unwrap(); | |
| 514 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::Chronological); | |
| 515 | + | let result = fg.get_all_items().await.unwrap(); | |
| 516 | 516 | assert_eq!(result.items[0].id.item_id, "rss:new"); | |
| 517 | 517 | assert_eq!(result.items[1].id.item_id, "rss:old"); | |
| 518 | 518 | } | |
| @@ -526,8 +526,8 @@ mod tests { | |||
| 526 | 526 | seed_item(&db, &feed, "rss:1", 0).await; | |
| 527 | 527 | seed_item(&db, &feed, "rss:2", 0).await; | |
| 528 | 528 | ||
| 529 | - | let gen = FeedGenerator::new(db); | |
| 530 | - | assert_eq!(gen.unread_count().await.unwrap(), 2); | |
| 529 | + | let fg = FeedGenerator::new(db); | |
| 530 | + | assert_eq!(fg.unread_count().await.unwrap(), 2); | |
| 531 | 531 | } | |
| 532 | 532 | ||
| 533 | 533 | #[tokio::test] | |
| @@ -539,8 +539,8 @@ mod tests { | |||
| 539 | 539 | ||
| 540 | 540 | db.items().mark_read(item.id, true).await.unwrap(); | |
| 541 | 541 | ||
| 542 | - | let gen = FeedGenerator::new(db); | |
| 543 | - | assert_eq!(gen.unread_count().await.unwrap(), 1); | |
| 542 | + | let fg = FeedGenerator::new(db); | |
| 543 | + | assert_eq!(fg.unread_count().await.unwrap(), 1); | |
| 544 | 544 | } | |
| 545 | 545 | ||
| 546 | 546 | // ── count with unread filter ──────────────────────────────── | |
| @@ -555,9 +555,9 @@ mod tests { | |||
| 555 | 555 | ||
| 556 | 556 | db.items().mark_read(item.id, true).await.unwrap(); | |
| 557 | 557 | ||
| 558 | - | let gen = FeedGenerator::new(db) | |
| 558 | + | let fg = FeedGenerator::new(db) | |
| 559 | 559 | .with_filter(FeedFilter::new().unread_only()); | |
| 560 | - | assert_eq!(gen.count().await.unwrap(), 2); | |
| 560 | + | assert_eq!(fg.count().await.unwrap(), 2); | |
| 561 | 561 | } | |
| 562 | 562 | ||
| 563 | 563 | // ── get_sources details ───────────────────────────────────── | |
| @@ -572,8 +572,8 @@ mod tests { | |||
| 572 | 572 | .await | |
| 573 | 573 | .unwrap(); | |
| 574 | 574 | ||
| 575 | - | let gen = FeedGenerator::new(db); | |
| 576 | - | let sources = gen.get_sources().await.unwrap(); | |
| 575 | + | let fg = FeedGenerator::new(db); | |
| 576 | + | let sources = fg.get_sources().await.unwrap(); | |
| 577 | 577 | assert_eq!(sources.len(), 1); | |
| 578 | 578 | assert!(sources[0].tags.contains(&"tech".to_string())); | |
| 579 | 579 | assert!(sources[0].tags.contains(&"rust".to_string())); | |
| @@ -588,8 +588,8 @@ mod tests { | |||
| 588 | 588 | ||
| 589 | 589 | db.items().mark_read(item.id, true).await.unwrap(); | |
| 590 | 590 | ||
| 591 | - | let gen = FeedGenerator::new(db); | |
| 592 | - | let sources = gen.get_sources().await.unwrap(); | |
| 591 | + | let fg = FeedGenerator::new(db); | |
| 592 | + | let sources = fg.get_sources().await.unwrap(); | |
| 593 | 593 | assert_eq!(sources[0].total_count, 2); | |
| 594 | 594 | assert_eq!(sources[0].unread_count, 1); | |
| 595 | 595 | } | |
| @@ -597,8 +597,8 @@ mod tests { | |||
| 597 | 597 | #[tokio::test] | |
| 598 | 598 | async fn get_sources_empty_db() { | |
| 599 | 599 | let db = test_db().await; | |
| 600 | - | let gen = FeedGenerator::new(db); | |
| 601 | - | let sources = gen.get_sources().await.unwrap(); | |
| 600 | + | let fg = FeedGenerator::new(db); | |
| 601 | + | let sources = fg.get_sources().await.unwrap(); | |
| 602 | 602 | assert!(sources.is_empty()); | |
| 603 | 603 | } | |
| 604 | 604 | ||
| @@ -614,8 +614,8 @@ mod tests { | |||
| 614 | 614 | seed_item(&db, &feed, "rss:mid", 5).await; | |
| 615 | 615 | seed_item(&db, &feed, "rss:new", 1).await; | |
| 616 | 616 | ||
| 617 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::Chronological); | |
| 618 | - | let result = gen.get_items(0).await.unwrap(); | |
| 617 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::Chronological); | |
| 618 | + | let result = fg.get_items(0).await.unwrap(); | |
| 619 | 619 | assert_eq!(result.items[0].id.item_id, "rss:new"); | |
| 620 | 620 | assert_eq!(result.items[1].id.item_id, "rss:mid"); | |
| 621 | 621 | assert_eq!(result.items[2].id.item_id, "rss:old"); | |
| @@ -629,8 +629,8 @@ mod tests { | |||
| 629 | 629 | seed_scored_item(&db, &feed, "rss:high", 2, Some(100)).await; | |
| 630 | 630 | seed_scored_item(&db, &feed, "rss:none", 3, None).await; | |
| 631 | 631 | ||
| 632 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::Score); | |
| 633 | - | let result = gen.get_items(0).await.unwrap(); | |
| 632 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::Score); | |
| 633 | + | let result = fg.get_items(0).await.unwrap(); | |
| 634 | 634 | assert_eq!(result.items[0].id.item_id, "rss:high"); | |
| 635 | 635 | assert_eq!(result.items[1].id.item_id, "rss:low"); | |
| 636 | 636 | assert_eq!(result.items[2].id.item_id, "rss:none"); | |
| @@ -643,8 +643,8 @@ mod tests { | |||
| 643 | 643 | seed_scored_item(&db, &feed, "rss:old_50", 10, Some(50)).await; | |
| 644 | 644 | seed_scored_item(&db, &feed, "rss:new_50", 1, Some(50)).await; | |
| 645 | 645 | ||
| 646 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::Score); | |
| 647 | - | let result = gen.get_items(0).await.unwrap(); | |
| 646 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::Score); | |
| 647 | + | let result = fg.get_items(0).await.unwrap(); | |
| 648 | 648 | // Same score, newer item should come first | |
| 649 | 649 | assert_eq!(result.items[0].id.item_id, "rss:new_50"); | |
| 650 | 650 | assert_eq!(result.items[1].id.item_id, "rss:old_50"); | |
| @@ -660,8 +660,8 @@ mod tests { | |||
| 660 | 660 | ||
| 661 | 661 | db.items().mark_read(read_item.id, true).await.unwrap(); | |
| 662 | 662 | ||
| 663 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::UnreadFirst); | |
| 664 | - | let result = gen.get_items(0).await.unwrap(); | |
| 663 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::UnreadFirst); | |
| 664 | + | let result = fg.get_items(0).await.unwrap(); | |
| 665 | 665 | assert_eq!(result.items.len(), 3); | |
| 666 | 666 | // Items should be grouped by read status: the sort uses | |
| 667 | 667 | // b.is_read.cmp(&a.is_read) which groups items by read state, | |
| @@ -685,8 +685,8 @@ mod tests { | |||
| 685 | 685 | ||
| 686 | 686 | db.items().mark_starred(starred_item.id, true).await.unwrap(); | |
| 687 | 687 | ||
| 688 | - | let gen = FeedGenerator::new(db).with_order(OrderBy::StarredFirst); | |
| 689 | - | let result = gen.get_items(0).await.unwrap(); | |
| 688 | + | let fg = FeedGenerator::new(db).with_order(OrderBy::StarredFirst); | |
| 689 | + | let result = fg.get_items(0).await.unwrap(); | |
| 690 | 690 | assert!(result.items[0].is_starred, "first item should be starred"); | |
| 691 | 691 | assert!(!result.items[1].is_starred, "second item should not be starred"); | |
| 692 | 692 | } | |
| @@ -701,9 +701,9 @@ mod tests { | |||
| 701 | 701 | seed_tagged_item(&db, &feed, "rss:python", 2, vec!["python".into()]).await; | |
| 702 | 702 | seed_tagged_item(&db, &feed, "rss:both", 3, vec!["rust".into(), "go".into()]).await; | |
| 703 | 703 | ||
| 704 | - | let gen = FeedGenerator::new(db) | |
| 704 | + | let fg = FeedGenerator::new(db) | |
| 705 | 705 | .with_filter(FeedFilter::new().with_tag("rust")); | |
| 706 | - | let result = gen.get_items(0).await.unwrap(); | |
| 706 | + | let result = fg.get_items(0).await.unwrap(); | |
| 707 | 707 | assert_eq!(result.items.len(), 2); | |
| 708 | 708 | let ids: Vec<&str> = result.items.iter().map(|i| i.id.item_id.as_str()).collect(); | |
| 709 | 709 | assert!(ids.contains(&"rss:rust")); | |
| @@ -716,9 +716,9 @@ mod tests { | |||
| 716 | 716 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 717 | 717 | seed_tagged_item(&db, &feed, "rss:1", 1, vec!["python".into()]).await; | |
| 718 | 718 | ||
| 719 | - | let gen = FeedGenerator::new(db) | |
| 719 | + | let fg = FeedGenerator::new(db) | |
| 720 | 720 | .with_filter(FeedFilter::new().with_tag("rust")); | |
| 721 | - | let result = gen.get_items(0).await.unwrap(); | |
| 721 | + | let result = fg.get_items(0).await.unwrap(); | |
| 722 | 722 | assert!(result.items.is_empty()); | |
| 723 | 723 | } | |
| 724 | 724 | ||
| @@ -731,9 +731,9 @@ mod tests { | |||
| 731 | 731 | seed_tagged_item(&db, &feed, "rss:p", 3, vec!["python".into()]).await; | |
| 732 | 732 | ||
| 733 | 733 | // Filter with two tags -- OR logic: items matching either tag pass | |
| 734 | - | let gen = FeedGenerator::new(db) | |
| 734 | + | let fg = FeedGenerator::new(db) | |
| 735 | 735 | .with_filter(FeedFilter::new().with_tag("rust").with_tag("go")); | |
| 736 | - | let result = gen.get_items(0).await.unwrap(); | |
| 736 | + | let result = fg.get_items(0).await.unwrap(); | |
| 737 | 737 | assert_eq!(result.items.len(), 2); | |
| 738 | 738 | } | |
| 739 | 739 | ||
| @@ -744,9 +744,9 @@ mod tests { | |||
| 744 | 744 | seed_item(&db, &feed, "rss:notagged", 1).await; // No tags | |
| 745 | 745 | seed_tagged_item(&db, &feed, "rss:tagged", 2, vec!["rust".into()]).await; | |
| 746 | 746 | ||
| 747 | - | let gen = FeedGenerator::new(db) | |
| 747 | + | let fg = FeedGenerator::new(db) | |
| 748 | 748 | .with_filter(FeedFilter::new().with_tag("rust")); | |
| 749 | - | let result = gen.get_items(0).await.unwrap(); | |
| 749 | + | let result = fg.get_items(0).await.unwrap(); | |
| 750 | 750 | assert_eq!(result.items.len(), 1); | |
| 751 | 751 | assert_eq!(result.items[0].id.item_id, "rss:tagged"); | |
| 752 | 752 | } | |
| @@ -767,10 +767,10 @@ mod tests { | |||
| 767 | 767 | seed_tagged_item(&db, &feed, &format!("rss:no_{i}"), i as i64, vec!["python".into()]).await; | |
| 768 | 768 | } | |
| 769 | 769 | ||
| 770 | - | let gen = FeedGenerator::new(db) | |
| 770 | + | let fg = FeedGenerator::new(db) | |
| 771 | 771 | .with_page_size(3) | |
| 772 | 772 | .with_filter(FeedFilter::new().with_tag("rust")); | |
| 773 | - | let result = gen.get_items(0).await.unwrap(); | |
| 773 | + | let result = fg.get_items(0).await.unwrap(); | |
| 774 | 774 | assert_eq!(result.items.len(), 1); | |
| 775 | 775 | // SQL returned 4 items (> page_size=3), so sql_had_more=true. | |
| 776 | 776 | // After in-memory tag filtering, only 1 item remains (< page_size). | |
| @@ -787,8 +787,8 @@ mod tests { | |||
| 787 | 787 | seed_item(&db, &feed, "rss:1", 0).await; | |
| 788 | 788 | seed_item(&db, &feed, "rss:2", 1).await; | |
| 789 | 789 | ||
| 790 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 791 | - | let result = gen.get_items(0).await.unwrap(); | |
| 790 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 791 | + | let result = fg.get_items(0).await.unwrap(); | |
| 792 | 792 | assert_eq!(result.items.len(), 2); | |
| 793 | 793 | assert!(!result.has_more); | |
| 794 | 794 | } | |
| @@ -802,10 +802,10 @@ mod tests { | |||
| 802 | 802 | seed_tagged_item(&db, &feed, "rss:1", 0, vec!["rust".into()]).await; | |
| 803 | 803 | seed_tagged_item(&db, &feed, "rss:2", 1, vec!["rust".into()]).await; | |
| 804 | 804 | ||
| 805 | - | let gen = FeedGenerator::new(db) | |
| 805 | + | let fg = FeedGenerator::new(db) | |
| 806 | 806 | .with_page_size(3) | |
| 807 | 807 | .with_filter(FeedFilter::new().with_tag("rust")); | |
| 808 | - | let result = gen.get_items(0).await.unwrap(); | |
| 808 | + | let result = fg.get_items(0).await.unwrap(); | |
| 809 | 809 | assert_eq!(result.items.len(), 2); | |
| 810 | 810 | assert!(!result.has_more); | |
| 811 | 811 | } | |
| @@ -821,9 +821,9 @@ mod tests { | |||
| 821 | 821 | seed_item(&db, &feed_a, "rss:2", 2).await; | |
| 822 | 822 | seed_item(&db, &feed_b, "hn:1", 3).await; | |
| 823 | 823 | ||
| 824 | - | let gen = FeedGenerator::new(db) | |
| 824 | + | let fg = FeedGenerator::new(db) | |
| 825 | 825 | .with_filter(FeedFilter::new().source("rss")); | |
| 826 | - | let result = gen.get_items(0).await.unwrap(); | |
| 826 | + | let result = fg.get_items(0).await.unwrap(); | |
| 827 | 827 | assert_eq!(result.items.len(), 2); | |
| 828 | 828 | assert!(result.items.iter().all(|i| i.id.source == "rss")); | |
| 829 | 829 | } | |
| @@ -837,9 +837,9 @@ mod tests { | |||
| 837 | 837 | seed_item(&db, &feed, "rss:1", 1).await; // Title: "Title rss:1" | |
| 838 | 838 | seed_item(&db, &feed, "rss:2", 2).await; // Title: "Title rss:2" | |
| 839 | 839 | ||
| 840 | - | let gen = FeedGenerator::new(db) | |
| 840 | + | let fg = FeedGenerator::new(db) | |
| 841 | 841 | .with_filter(FeedFilter::new().search("Title rss:1")); | |
| 842 | - | let result = gen.get_items(0).await.unwrap(); | |
| 842 | + | let result = fg.get_items(0).await.unwrap(); | |
| 843 | 843 | assert_eq!(result.items.len(), 1); | |
| 844 | 844 | assert_eq!(result.items[0].id.item_id, "rss:1"); | |
| 845 | 845 | } | |
| @@ -850,9 +850,9 @@ mod tests { | |||
| 850 | 850 | let feed = seed_feed(&db, "rss", "Feed").await; | |
| 851 | 851 | seed_item(&db, &feed, "rss:1", 1).await; | |
| 852 | 852 | ||
| 853 | - | let gen = FeedGenerator::new(db) | |
| 853 | + | let fg = FeedGenerator::new(db) | |
| 854 | 854 | .with_filter(FeedFilter::new().search("zzz_nonexistent_zzz")); | |
| 855 | - | let result = gen.get_items(0).await.unwrap(); | |
| 855 | + | let result = fg.get_items(0).await.unwrap(); | |
| 856 | 856 | assert!(result.items.is_empty()); | |
| 857 | 857 | } | |
| 858 | 858 | ||
| @@ -867,9 +867,9 @@ mod tests { | |||
| 867 | 867 | seed_item(&db, &feed_b, "hn:match", 2).await; | |
| 868 | 868 | ||
| 869 | 869 | // Search that matches both, but restricted to rss source | |
| 870 | - | let gen = FeedGenerator::new(db) | |
| 870 | + | let fg = FeedGenerator::new(db) | |
| 871 | 871 | .with_filter(FeedFilter::new().search("Item").source("rss")); | |
| 872 | - | let result = gen.get_items(0).await.unwrap(); | |
| 872 | + | let result = fg.get_items(0).await.unwrap(); | |
| 873 | 873 | assert_eq!(result.items.len(), 1); | |
| 874 | 874 | assert_eq!(result.items[0].id.source, "rss"); | |
| 875 | 875 | } | |
| @@ -884,9 +884,9 @@ mod tests { | |||
| 884 | 884 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 885 | 885 | ||
| 886 | 886 | // Search matches both items, but only unread should appear | |
| 887 | - | let gen = FeedGenerator::new(db) | |
| 887 | + | let fg = FeedGenerator::new(db) | |
| 888 | 888 | .with_filter(FeedFilter::new().search("Item").unread_only()); | |
| 889 | - | let result = gen.get_items(0).await.unwrap(); | |
| 889 | + | let result = fg.get_items(0).await.unwrap(); | |
| 890 | 890 | assert_eq!(result.items.len(), 1); | |
| 891 | 891 | assert!(!result.items[0].is_read); | |
| 892 | 892 | } | |
| @@ -900,9 +900,9 @@ mod tests { | |||
| 900 | 900 | ||
| 901 | 901 | db.items().mark_starred(item2.id, true).await.unwrap(); | |
| 902 | 902 | ||
| 903 | - | let gen = FeedGenerator::new(db) | |
| 903 | + | let fg = FeedGenerator::new(db) | |
| 904 | 904 | .with_filter(FeedFilter::new().search("Item").starred_only()); | |
| 905 | - | let result = gen.get_items(0).await.unwrap(); | |
| 905 | + | let result = fg.get_items(0).await.unwrap(); | |
| 906 | 906 | assert_eq!(result.items.len(), 1); | |
| 907 | 907 | assert!(result.items[0].is_starred); | |
| 908 | 908 | } | |
| @@ -912,8 +912,8 @@ mod tests { | |||
| 912 | 912 | #[tokio::test] | |
| 913 | 913 | async fn get_all_items_empty_db() { | |
| 914 | 914 | let db = test_db().await; | |
| 915 | - | let gen = FeedGenerator::new(db); | |
| 916 | - | let result = gen.get_all_items().await.unwrap(); | |
| 915 | + | let fg = FeedGenerator::new(db); | |
| 916 | + | let result = fg.get_all_items().await.unwrap(); | |
| 917 | 917 | assert!(result.items.is_empty()); | |
| 918 | 918 | assert!(!result.has_more); | |
| 919 | 919 | } | |
| @@ -928,9 +928,9 @@ mod tests { | |||
| 928 | 928 | ||
| 929 | 929 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 930 | 930 | ||
| 931 | - | let gen = FeedGenerator::new(db) | |
| 931 | + | let fg = FeedGenerator::new(db) | |
| 932 | 932 | .with_filter(FeedFilter::new().unread_only()); | |
| 933 | - | let result = gen.get_all_items().await.unwrap(); | |
| 933 | + | let result = fg.get_all_items().await.unwrap(); | |
| 934 | 934 | assert_eq!(result.items.len(), 2); | |
| 935 | 935 | assert!(result.items.iter().all(|i| !i.is_read)); | |
| 936 | 936 | } | |
| @@ -944,9 +944,9 @@ mod tests { | |||
| 944 | 944 | ||
| 945 | 945 | db.items().mark_starred(item2.id, true).await.unwrap(); | |
| 946 | 946 | ||
| 947 | - | let gen = FeedGenerator::new(db) | |
| 947 | + | let fg = FeedGenerator::new(db) | |
| 948 | 948 | .with_filter(FeedFilter::new().starred_only()); | |
| 949 | - | let result = gen.get_all_items().await.unwrap(); | |
| 949 | + | let result = fg.get_all_items().await.unwrap(); | |
| 950 | 950 | assert_eq!(result.items.len(), 1); | |
| 951 | 951 | assert!(result.items[0].is_starred); | |
| 952 | 952 | } | |
| @@ -958,9 +958,9 @@ mod tests { | |||
| 958 | 958 | seed_tagged_item(&db, &feed, "rss:tagged", 1, vec!["rust".into()]).await; | |
| 959 | 959 | seed_item(&db, &feed, "rss:plain", 2).await; | |
| 960 | 960 | ||
| 961 | - | let gen = FeedGenerator::new(db) | |
| 961 | + | let fg = FeedGenerator::new(db) | |
| 962 | 962 | .with_filter(FeedFilter::new().with_tag("rust")); | |
| 963 | - | let result = gen.get_all_items().await.unwrap(); | |
| 963 | + | let result = fg.get_all_items().await.unwrap(); | |
| 964 | 964 | assert_eq!(result.items.len(), 1); | |
| 965 | 965 | assert_eq!(result.items[0].id.item_id, "rss:tagged"); | |
| 966 | 966 | } | |
| @@ -972,9 +972,9 @@ mod tests { | |||
| 972 | 972 | seed_item(&db, &feed, "rss:1", 1).await; | |
| 973 | 973 | seed_item(&db, &feed, "rss:2", 2).await; | |
| 974 | 974 | ||
| 975 | - | let gen = FeedGenerator::new(db) | |
| 975 | + | let fg = FeedGenerator::new(db) | |
| 976 | 976 | .with_filter(FeedFilter::new().search("Item rss:1")); | |
| 977 | - | let result = gen.get_all_items().await.unwrap(); | |
| 977 | + | let result = fg.get_all_items().await.unwrap(); | |
| 978 | 978 | assert_eq!(result.items.len(), 1); | |
| 979 | 979 | } | |
| 980 | 980 | ||
| @@ -989,9 +989,9 @@ mod tests { | |||
| 989 | 989 | ||
| 990 | 990 | db.items().mark_read(item_a1.id, true).await.unwrap(); | |
| 991 | 991 | ||
| 992 | - | let gen = FeedGenerator::new(db) | |
| 992 | + | let fg = FeedGenerator::new(db) |
Lines truncated
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "balanced-breakfast-desktop" | |
| 3 | 3 | version = "0.3.0" | |
| 4 | - | edition = "2021" | |
| 4 | + | edition = "2024" | |
| 5 | 5 | ||
| 6 | 6 | [[bin]] | |
| 7 | 7 | name = "balanced-breakfast-desktop" |
| @@ -149,6 +149,7 @@ body { | |||
| 149 | 149 | } | |
| 150 | 150 | .btn-success:hover { background-color: var(--accent-yellow-light); border-color: var(--accent-yellow-light); } | |
| 151 | 151 | .btn-small { padding: 0.2rem 0.5rem; font-size: 0.8rem; } | |
| 152 | + | .btn-small.active { background: var(--accent-yellow); color: var(--text-on-accent); border-color: var(--accent-yellow); } | |
| 152 | 153 | ||
| 153 | 154 | /* Main layout */ | |
| 154 | 155 | .main { | |
| @@ -865,7 +866,7 @@ body { | |||
| 865 | 866 | ||
| 866 | 867 | /* Source edit button (gear icon) */ | |
| 867 | 868 | .source-edit { | |
| 868 | - | display: none; | |
| 869 | + | display: inline; | |
| 869 | 870 | background: none; | |
| 870 | 871 | border: none; | |
| 871 | 872 | color: var(--text-muted); | |
| @@ -873,9 +874,9 @@ body { | |||
| 873 | 874 | font-size: 0.8rem; | |
| 874 | 875 | padding: 0 0.2rem; | |
| 875 | 876 | line-height: 1; | |
| 877 | + | opacity: 0.4; | |
| 876 | 878 | } | |
| 877 | - | .source-edit:hover { color: var(--accent-yellow); } | |
| 878 | - | .source-item:hover .source-edit { display: inline; } | |
| 879 | + | .source-edit:hover { color: var(--accent-yellow); opacity: 1; } | |
| 879 | 880 | ||
| 880 | 881 | /* Query feeds — sidebar divider */ | |
| 881 | 882 | .source-divider { |
| @@ -16,14 +16,17 @@ | |||
| 16 | 16 | <input type="text" id="search-input" placeholder="Search... (/) | ? for help" class="search-input"> | |
| 17 | 17 | <span id="search-spinner" class="search-spinner" aria-hidden="true"></span> | |
| 18 | 18 | <label for="sort-select" class="sr-only">Sort order</label> | |
| 19 | - | <select id="sort-select" class="sort-select" title="Sort order"> | |
| 19 | + | <button id="unread-toggle" class="btn btn-small" title="Show unread only (U)" aria-pressed="false">Unread</button> | |
| 20 | + | <select id="sort-select" class="sort-select" title="Sort order (S)"> | |
| 20 | 21 | <option value="chronological">Newest First</option> | |
| 21 | 22 | <option value="score">By Score</option> | |
| 22 | 23 | <option value="unread">Unread First</option> | |
| 23 | 24 | <option value="starred">Starred First</option> | |
| 24 | 25 | </select> | |
| 25 | - | <button id="refresh-btn" class="btn btn-primary" title="Refresh all feeds">Refresh</button> | |
| 26 | + | <button id="mark-all-read-btn" class="btn btn-small" title="Mark all as read (Shift+A)">Mark All Read</button> | |
| 27 | + | <button id="refresh-btn" class="btn btn-primary" title="Refresh all feeds (R)">Refresh</button> | |
| 26 | 28 | <button id="add-feed-btn" class="btn btn-success" title="Add a new feed source">+ Add Feed</button> | |
| 29 | + | <button id="help-btn" class="btn btn-small" title="Keyboard shortcuts (?)" aria-label="Help">?</button> | |
| 27 | 30 | <button id="settings-btn" class="btn btn-small" title="Settings" aria-label="Settings">⚙</button> | |
| 28 | 31 | </div> | |
| 29 | 32 | </header> | |
| @@ -41,8 +44,8 @@ | |||
| 41 | 44 | </li> | |
| 42 | 45 | </ul> | |
| 43 | 46 | </nav> | |
| 44 | - | <div id="saved-articles-btn" class="sidebar-saved" title="Reading List"> | |
| 45 | - | <span class="source-name">Reading List</span> | |
| 47 | + | <div id="saved-articles-btn" class="sidebar-saved" title="Saved Articles"> | |
| 48 | + | <span class="source-name">Saved Articles</span> | |
| 46 | 49 | <span id="saved-count" class="source-count">0</span> | |
| 47 | 50 | </div> | |
| 48 | 51 | <div class="sidebar-footer"> |
| @@ -84,6 +84,9 @@ | |||
| 84 | 84 | document.getElementById('load-more-btn').addEventListener('click', BB.items.loadMore); | |
| 85 | 85 | document.getElementById('settings-btn').addEventListener('click', showSettings); | |
| 86 | 86 | document.getElementById('sync-settings-btn').addEventListener('click', BB.sync.openSettings); | |
| 87 | + | document.getElementById('help-btn').addEventListener('click', showHelp); | |
| 88 | + | document.getElementById('unread-toggle').addEventListener('click', toggleUnreadFilter); | |
| 89 | + | document.getElementById('mark-all-read-btn').addEventListener('click', markAllReadGlobal); | |
| 87 | 90 | ||
| 88 | 91 | // Modal close on overlay click | |
| 89 | 92 | document.getElementById('modal-overlay').addEventListener('click', (e) => { | |
| @@ -106,6 +109,39 @@ | |||
| 106 | 109 | } | |
| 107 | 110 | ||
| 108 | 111 | /** | |
| 112 | + | * Toggle the "unread only" filter on/off. | |
| 113 | + | */ | |
| 114 | + | function toggleUnreadFilter() { | |
| 115 | + | const btn = document.getElementById('unread-toggle'); | |
| 116 | + | const active = btn.getAttribute('aria-pressed') === 'true'; | |
| 117 | + | btn.setAttribute('aria-pressed', !active); | |
| 118 | + | btn.classList.toggle('active', !active); | |
| 119 | + | BB.state.set('unreadOnly', !active); | |
| 120 | + | BB.state.resetPagination(true); | |
| 121 | + | BB.items.load(); | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | /** | |
| 125 | + | * Mark all items as read (global or per-source). | |
| 126 | + | */ | |
| 127 | + | async function markAllReadGlobal() { | |
| 128 | + | const source = BB.state.currentSource || null; | |
| 129 | + | const label = source | |
| 130 | + | ? (BB.state.sources.find(s => s.id === source) || {}).name || 'this source' | |
| 131 | + | : 'all sources'; | |
| 132 | + | const ok = await BB.ui.confirmAction('Mark all items in ' + label + ' as read?'); | |
| 133 | + | if (!ok) return; | |
| 134 | + | try { | |
| 135 | + | await BB.api.items.markAllRead(source); | |
| 136 | + | BB.ui.showToast('Marked all as read'); | |
| 137 | + | await BB.sources.load(); | |
| 138 | + | await BB.items.load(); | |
| 139 | + | } catch (err) { | |
| 140 | + | BB.ui.showToast('Failed: ' + BB.utils.getErrorMessage(err), 'error'); | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | /** | |
| 109 | 145 | * Global keyboard shortcut handler. Skipped when focus is in a form input. | |
| 110 | 146 | * Key choices follow common reader conventions: j/k from vim, s/r from | |
| 111 | 147 | * Google Reader, /=search from vim, ?=help from many CLI tools. | |
| @@ -166,6 +202,18 @@ | |||
| 166 | 202 | } | |
| 167 | 203 | break; | |
| 168 | 204 | ||
| 205 | + | case 'u': // Toggle unread only | |
| 206 | + | e.preventDefault(); | |
| 207 | + | toggleUnreadFilter(); | |
| 208 | + | break; | |
| 209 | + | ||
| 210 | + | case 'A': // Mark all as read (Shift+A) | |
| 211 | + | if (e.shiftKey) { | |
| 212 | + | e.preventDefault(); | |
| 213 | + | markAllReadGlobal(); | |
| 214 | + | } | |
| 215 | + | break; | |
| 216 | + | ||
| 169 | 217 | case '/': // Focus search | |
| 170 | 218 | e.preventDefault(); | |
| 171 | 219 | document.getElementById('search-input').focus(); | |
| @@ -407,6 +455,8 @@ | |||
| 407 | 455 | <h3>Actions</h3> | |
| 408 | 456 | <div class="help-row"><kbd>s</kbd><span>Star / unstar</span></div> | |
| 409 | 457 | <div class="help-row"><kbd>r</kbd><span>Toggle read / unread</span></div> | |
| 458 | + | <div class="help-row"><kbd>u</kbd><span>Toggle unread-only filter</span></div> | |
| 459 | + | <div class="help-row"><kbd>Shift</kbd> <kbd>A</kbd><span>Mark all as read</span></div> | |
| 410 | 460 | <div class="help-row"><kbd>?</kbd><span>Show this help</span></div> | |
| 411 | 461 | </div> | |
| 412 | 462 | <div class="help-section"> |
| @@ -413,7 +413,8 @@ | |||
| 413 | 413 | * @param {string} id - Bookmark ID. | |
| 414 | 414 | */ | |
| 415 | 415 | async function deleteBookmark(id) { | |
| 416 | - | if (!confirm('Remove this bookmark?')) return; | |
| 416 | + | const ok = await BB.ui.confirmAction('Remove this bookmark?'); | |
| 417 | + | if (!ok) return; | |
| 417 | 418 | try { | |
| 418 | 419 | await BB.api.bookmarks.delete(id); | |
| 419 | 420 | BB.ui.showToast('Bookmark removed'); |
| @@ -83,16 +83,27 @@ | |||
| 83 | 83 | const body = document.getElementById('modal-body'); | |
| 84 | 84 | ||
| 85 | 85 | title.textContent = 'Add Feed'; | |
| 86 | - | body.innerHTML = '<p style="margin-bottom: 1rem; color: var(--text-secondary);">Select a plugin:</p>'; | |
| 86 | + | body.innerHTML = '<p style="margin-bottom: 1rem; color: var(--text-secondary);">Select a source type:</p>'; | |
| 87 | 87 | ||
| 88 | 88 | const list = document.createElement('ul'); | |
| 89 | 89 | list.className = 'plugin-list'; | |
| 90 | 90 | ||
| 91 | - | plugins.forEach(plugin => { | |
| 91 | + | // Sort recommended plugins first | |
| 92 | + | const recommended = new Set(['rss', 'mastodon', 'reddit']); | |
| 93 | + | const sorted = [...plugins].sort((a, b) => { | |
| 94 | + | const aRec = recommended.has(a.id) ? 0 : 1; | |
| 95 | + | const bRec = recommended.has(b.id) ? 0 : 1; | |
| 96 | + | return aRec - bRec || a.name.localeCompare(b.name); | |
| 97 | + | }); | |
| 98 | + | ||
| 99 | + | sorted.forEach(plugin => { | |
| 92 | 100 | const li = document.createElement('li'); | |
| 93 | 101 | li.className = 'plugin-item'; | |
| 102 | + | const badge = recommended.has(plugin.id) | |
| 103 | + | ? '<span style="font-size:0.7rem; padding:0.1rem 0.4rem; background:var(--accent-yellow); color:var(--text-on-accent); border-radius:3px; margin-left:0.5rem; vertical-align:middle;">Recommended</span>' | |
| 104 | + | : ''; | |
| 94 | 105 | li.innerHTML = ` | |
| 95 | - | <div class="plugin-name">${BB.utils.escapeHtml(plugin.name)}</div> | |
| 106 | + | <div class="plugin-name">${BB.utils.escapeHtml(plugin.name)}${badge}</div> | |
| 96 | 107 | ${plugin.description ? `<div class="plugin-desc">${BB.utils.escapeHtml(plugin.description)}</div>` : ''} | |
| 97 | 108 | `; | |
| 98 | 109 | li.onclick = () => selectPlugin(plugin.id); | |
| @@ -158,8 +169,9 @@ | |||
| 158 | 169 | config: data, | |
| 159 | 170 | }); | |
| 160 | 171 | ||
| 161 | - | BB.ui.showToast('Feed created!'); | |
| 162 | - | BB.sources.load(); | |
| 172 | + | BB.ui.showToast('Feed created! Fetching articles...'); | |
| 173 | + | await BB.sources.load(); | |
| 174 | + | refresh(); | |
| 163 | 175 | }, | |
| 164 | 176 | }); | |
| 165 | 177 | } |
| @@ -18,9 +18,11 @@ | |||
| 18 | 18 | * @returns {Object} Filter object for the list_items API call. | |
| 19 | 19 | */ | |
| 20 | 20 | function buildFilter(page) { | |
| 21 | + | const unreadFromSort = BB.state.currentSource === '' && BB.state.currentOrder === 'unread'; | |
| 22 | + | const unreadToggle = BB.state.unreadOnly; | |
| 21 | 23 | return { | |
| 22 | 24 | source: BB.state.currentSource || undefined, | |
| 23 | - | unread: BB.state.currentSource === '' && BB.state.currentOrder === 'unread' ? true : undefined, | |
| 25 | + | unread: (unreadFromSort || unreadToggle) ? true : undefined, | |
| 24 | 26 | starred: BB.state.currentSource === '' && BB.state.currentOrder === 'starred' ? true : undefined, | |
| 25 | 27 | search: BB.state.currentSearch || undefined, | |
| 26 | 28 | order: BB.state.currentOrder || undefined, | |
| @@ -96,9 +98,17 @@ | |||
| 96 | 98 | ||
| 97 | 99 | if (items.length === 0) { | |
| 98 | 100 | const hasFeeds = BB.state.sources && BB.state.sources.length > 0; | |
| 99 | - | const message = hasFeeds | |
| 100 | - | ? '<div class="empty-icon">🔍</div>No items match the current filter.<br>Try switching to "All" or a different source.' | |
| 101 | - | : '<div class="empty-icon">🍳</div>No feeds yet.<br>Click <strong>+ Add Feed</strong> to get started, or import an OPML file.<br><kbd>?</kbd> for keyboard shortcuts.'; | |
| 101 | + | const totalItems = hasFeeds ? BB.state.sources.reduce((n, s) => n + s.totalCount, 0) : 0; | |
| 102 | + | let message; | |
| 103 | + | if (!hasFeeds) { | |
| 104 | + | message = '<div class="empty-icon">🍳</div>No feeds yet.<br>Click <strong>+ Add Feed</strong> to get started, or import an OPML file.<br><kbd>?</kbd> for keyboard shortcuts.'; | |
| 105 | + | } else if (totalItems === 0) { | |
| 106 | + | message = '<div class="empty-icon">🔄</div>Feeds added but no articles yet.<br>Click <strong>Refresh</strong> to fetch posts.'; | |
| 107 | + | } else if (BB.state.unreadOnly) { | |
| 108 | + | message = '<div class="empty-icon">✓</div>All caught up! No unread items.'; | |
| 109 | + | } else { | |
| 110 | + | message = '<div class="empty-icon">🔍</div>No items match the current filter.<br>Try switching to "All" or a different source.'; | |
| 111 | + | } | |
| 102 | 112 | list.innerHTML = '<li class="item empty-state">' + message + '</li>'; | |
| 103 | 113 | document.getElementById('load-more').style.display = 'none'; | |
| 104 | 114 | return; |
| @@ -77,7 +77,7 @@ | |||
| 77 | 77 | const emptyLi = document.createElement('li'); | |
| 78 | 78 | emptyLi.className = 'source-item'; | |
| 79 | 79 | emptyLi.style.cssText = 'cursor:default; flex-direction:column; align-items:center; text-align:center; padding:1.5rem 1rem; color:var(--text-muted); font-size:0.8rem; line-height:1.5;'; | |
| 80 | - | emptyLi.innerHTML = '<div style="font-size:1.5rem; margin-bottom:0.5rem; opacity:0.5;">\uD83C\uDF73</div>Add your first feed to get started.<br>Click <strong style="color:var(--yolk)">+ Add Feed</strong> above.'; | |
| 80 | + | emptyLi.innerHTML = '<div style="font-size:1.5rem; margin-bottom:0.5rem; opacity:0.5;">\uD83C\uDF73</div>Add your first feed to get started.<br>Click <strong style="color:var(--yolk)">+ Add Feed</strong> above,<br>or <a href="#" onclick="event.preventDefault(); BB.feeds.importOpml();" style="color:var(--accent-yellow);">import an OPML file</a>.'; | |
| 81 | 81 | frag.appendChild(emptyLi); | |
| 82 | 82 | } | |
| 83 | 83 | ||
| @@ -177,10 +177,10 @@ | |||
| 177 | 177 | }); | |
| 178 | 178 | } | |
| 179 | 179 | ||
| 180 | - | // "+ Query Feed" button | |
| 180 | + | // "+ Saved Filter" button | |
| 181 | 181 | const addQfBtn = document.createElement('li'); | |
| 182 | 182 | addQfBtn.className = 'source-item add-query-feed-btn'; | |
| 183 | - | addQfBtn.innerHTML = '<span class="source-name" style="color:var(--text-muted)">+ Query Feed</span>'; | |
| 183 | + | addQfBtn.innerHTML = '<span class="source-name" style="color:var(--text-muted)" title="Create a dynamic feed from search filters">+ Saved Filter</span>'; | |
| 184 | 184 | addQfBtn.onclick = () => BB.queryFeeds.openBuilder(null); | |
| 185 | 185 | frag.appendChild(addQfBtn); | |
| 186 | 186 | ||
| @@ -245,6 +245,7 @@ | |||
| 245 | 245 | ||
| 246 | 246 | // Action buttons | |
| 247 | 247 | const actions = `<div class="hp-actions"> | |
| 248 | + | <button class="hp-action" data-action="mark-read">Mark All Read</button> | |
| 248 | 249 | <button class="hp-action" data-action="edit">Edit Feed</button> | |
| 249 | 250 | <button class="hp-action" data-action="tags">Edit Tags</button> | |
| 250 | 251 | <button class="hp-action hp-action-danger" data-action="delete">Delete</button> | |
| @@ -263,6 +264,17 @@ | |||
| 263 | 264 | pop.remove(); | |
| 264 | 265 | document.removeEventListener('click', outsideClick, true); | |
| 265 | 266 | } | |
| 267 | + | pop.querySelector('[data-action="mark-read"]').onclick = async () => { | |
| 268 | + | dismiss(); | |
| 269 | + | try { | |
| 270 | + | await BB.api.items.markAllRead(source.id); | |
| 271 | + | BB.ui.showToast('Marked all read in ' + source.name); | |
| 272 | + | await BB.sources.load(); | |
| 273 | + | await BB.items.load(); | |
| 274 | + | } catch (err) { | |
| 275 | + | BB.ui.showToast('Failed: ' + getErrorMessage(err), 'error'); | |
| 276 | + | } | |
| 277 | + | }; | |
| 266 | 278 | pop.querySelector('[data-action="edit"]').onclick = () => { dismiss(); editFeed(source); }; | |
| 267 | 279 | pop.querySelector('[data-action="tags"]').onclick = () => { dismiss(); editTags(source); }; | |
| 268 | 280 | pop.querySelector('[data-action="delete"]').onclick = () => { dismiss(); deleteFeed(source); }; |
| @@ -353,6 +353,21 @@ pub async fn unstar_item( | |||
| 353 | 353 | Ok(state.orchestrator.database().items().mark_starred(item_id, false).await?) | |
| 354 | 354 | } | |
| 355 | 355 | ||
| 356 | + | /// Mark all unread items as read, optionally for a specific source. | |
| 357 | + | #[tauri::command] | |
| 358 | + | #[instrument(skip_all)] | |
| 359 | + | pub async fn mark_all_read( | |
| 360 | + | state: State<'_, Arc<AppState>>, | |
| 361 | + | source_id: Option<String>, | |
| 362 | + | ) -> Result<u64, ApiError> { | |
| 363 | + | Ok(state | |
| 364 | + | .orchestrator | |
| 365 | + | .database() | |
| 366 | + | .items() | |
| 367 | + | .mark_all_read(source_id.as_deref()) | |
| 368 | + | .await?) | |
| 369 | + | } | |
| 370 | + | ||
| 356 | 371 | /// Get the total count of unread items. | |
| 357 | 372 | #[tauri::command] | |
| 358 | 373 | #[instrument(skip_all)] |
| @@ -17,22 +17,22 @@ async fn generator_mark_read_updates_unread_count() { | |||
| 17 | 17 | common::insert_item(&db, &feed, "rss:mr2", "Article 2", 2).await; | |
| 18 | 18 | common::insert_item(&db, &feed, "rss:mr3", "Article 3", 3).await; | |
| 19 | 19 | ||
| 20 | - | let gen = FeedGenerator::new(db.clone()); | |
| 20 | + | let fg = FeedGenerator::new(db.clone()); | |
| 21 | 21 | ||
| 22 | 22 | // All 3 unread initially | |
| 23 | - | assert_eq!(gen.unread_count().await.unwrap(), 3); | |
| 23 | + | assert_eq!(fg.unread_count().await.unwrap(), 3); | |
| 24 | 24 | ||
| 25 | 25 | // Mark one read via generator (uses external_id lookup) | |
| 26 | - | gen.mark_read("rss:mr1", true).await.unwrap(); | |
| 27 | - | assert_eq!(gen.unread_count().await.unwrap(), 2); | |
| 26 | + | fg.mark_read("rss:mr1", true).await.unwrap(); | |
| 27 | + | assert_eq!(fg.unread_count().await.unwrap(), 2); | |
| 28 | 28 | ||
| 29 | 29 | // Mark another read | |
| 30 | - | gen.mark_read("rss:mr2", true).await.unwrap(); | |
| 31 | - | assert_eq!(gen.unread_count().await.unwrap(), 1); | |
| 30 | + | fg.mark_read("rss:mr2", true).await.unwrap(); | |
| 31 | + | assert_eq!(fg.unread_count().await.unwrap(), 1); | |
| 32 | 32 | ||
| 33 | 33 | // Mark one back to unread | |
| 34 | - | gen.mark_read("rss:mr1", false).await.unwrap(); | |
| 35 | - | assert_eq!(gen.unread_count().await.unwrap(), 2); | |
| 34 | + | fg.mark_read("rss:mr1", false).await.unwrap(); | |
| 35 | + | assert_eq!(fg.unread_count().await.unwrap(), 2); | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | #[tokio::test] | |
| @@ -42,15 +42,15 @@ async fn generator_mark_starred_does_not_affect_unread_count() { | |||
| 42 | 42 | common::insert_item(&db, &feed, "rss:ms1", "Article 1", 1).await; | |
| 43 | 43 | common::insert_item(&db, &feed, "rss:ms2", "Article 2", 2).await; | |
| 44 | 44 | ||
| 45 | - | let gen = FeedGenerator::new(db.clone()); | |
| 46 | - | assert_eq!(gen.unread_count().await.unwrap(), 2); | |
| 45 | + | let fg = FeedGenerator::new(db.clone()); | |
| 46 | + | assert_eq!(fg.unread_count().await.unwrap(), 2); | |
| 47 | 47 | ||
| 48 | 48 | // Starring should not change unread count | |
| 49 | - | gen.mark_starred("rss:ms1", true).await.unwrap(); | |
| 50 | - | assert_eq!(gen.unread_count().await.unwrap(), 2); | |
| 49 | + | fg.mark_starred("rss:ms1", true).await.unwrap(); | |
| 50 | + | assert_eq!(fg.unread_count().await.unwrap(), 2); | |
| 51 | 51 | ||
| 52 | - | gen.mark_starred("rss:ms1", false).await.unwrap(); | |
| 53 | - | assert_eq!(gen.unread_count().await.unwrap(), 2); | |
| 52 | + | fg.mark_starred("rss:ms1", false).await.unwrap(); | |
| 53 | + | assert_eq!(fg.unread_count().await.unwrap(), 2); | |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | 56 | #[tokio::test] | |
| @@ -59,11 +59,11 @@ async fn generator_mark_read_nonexistent_preserves_count() { | |||
| 59 | 59 | let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; | |
| 60 | 60 | common::insert_item(&db, &feed, "rss:1", "Article", 1).await; | |
| 61 | 61 | ||
| 62 | - | let gen = FeedGenerator::new(db.clone()); | |
| 62 | + | let fg = FeedGenerator::new(db.clone()); | |
| 63 | 63 | // Should not panic or error | |
| 64 | - | gen.mark_read("nonexistent:999", true).await.unwrap(); | |
| 64 | + | fg.mark_read("nonexistent:999", true).await.unwrap(); | |
| 65 | 65 | // Original item should remain unread | |
| 66 | - | assert_eq!(gen.unread_count().await.unwrap(), 1); | |
| 66 | + | assert_eq!(fg.unread_count().await.unwrap(), 1); | |
| 67 | 67 | } | |
| 68 | 68 | ||
| 69 | 69 | // ── Ordering ───────────────────────────────────────────────────────── | |
| @@ -102,8 +102,8 @@ async fn list_items_order_by_score() { | |||
| 102 | 102 | .unwrap(); | |
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | - | let gen = FeedGenerator::new(db).with_order(bb_feed::OrderBy::Score); | |
| 106 | - | let result = gen.get_items(0).await.unwrap(); | |
| 105 | + | let fg = FeedGenerator::new(db).with_order(bb_feed::OrderBy::Score); | |
| 106 | + | let result = fg.get_items(0).await.unwrap(); | |
| 107 | 107 | assert_eq!(result.items.len(), 3); | |
| 108 | 108 | assert_eq!(result.items[0].meta.score, Some(100)); | |
| 109 | 109 | assert_eq!(result.items[1].meta.score, Some(10)); | |
| @@ -119,8 +119,8 @@ async fn list_items_order_starred_first() { | |||
| 119 | 119 | ||
| 120 | 120 | db.items().mark_starred(item2.id, true).await.unwrap(); | |
| 121 | 121 | ||
| 122 | - | let gen = FeedGenerator::new(db).with_order(bb_feed::OrderBy::StarredFirst); | |
| 123 | - | let result = gen.get_items(0).await.unwrap(); | |
| 122 | + | let fg = FeedGenerator::new(db).with_order(bb_feed::OrderBy::StarredFirst); | |
| 123 | + | let result = fg.get_items(0).await.unwrap(); | |
| 124 | 124 | assert!(result.items[0].is_starred, "starred item should sort first"); | |
| 125 | 125 | assert!(!result.items[1].is_starred); | |
| 126 | 126 | // item1 should not have id leakage | |
| @@ -136,8 +136,8 @@ async fn list_items_order_unread_first() { | |||
| 136 | 136 | ||
| 137 | 137 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 138 | 138 | ||
| 139 | - | let gen = FeedGenerator::new(db).with_order(bb_feed::OrderBy::UnreadFirst); | |
| 140 | - | let result = gen.get_items(0).await.unwrap(); | |
| 139 | + | let fg = FeedGenerator::new(db).with_order(bb_feed::OrderBy::UnreadFirst); | |
| 140 | + | let result = fg.get_items(0).await.unwrap(); | |
| 141 | 141 | // UnreadFirst groups items by read status — verify they're grouped | |
| 142 | 142 | let first_read = result.items[0].is_read; | |
| 143 | 143 | let last_read = result.items[1].is_read; | |
| @@ -154,8 +154,8 @@ async fn generator_count_all() { | |||
| 154 | 154 | common::insert_item(&db, &feed, "rss:2", "B", 2).await; | |
| 155 | 155 | common::insert_item(&db, &feed, "rss:3", "C", 3).await; | |
| 156 | 156 | ||
| 157 | - | let gen = FeedGenerator::new(db); | |
| 158 | - | assert_eq!(gen.count().await.unwrap(), 3); | |
| 157 | + | let fg = FeedGenerator::new(db); | |
| 158 | + | assert_eq!(fg.count().await.unwrap(), 3); | |
| 159 | 159 | } | |
| 160 | 160 | ||
| 161 | 161 | #[tokio::test] | |
| @@ -167,8 +167,8 @@ async fn generator_count_with_source_filter() { | |||
| 167 | 167 | common::insert_item(&db, &feed_hn, "hn:1", "B", 1).await; | |
| 168 | 168 | common::insert_item(&db, &feed_hn, "hn:2", "C", 2).await; | |
| 169 | 169 | ||
| 170 | - | let gen = FeedGenerator::new(db.clone()).with_filter(FeedFilter::new().source("hn")); | |
| 171 | - | assert_eq!(gen.count().await.unwrap(), 2); | |
| 170 | + | let fg = FeedGenerator::new(db.clone()).with_filter(FeedFilter::new().source("hn")); | |
| 171 | + | assert_eq!(fg.count().await.unwrap(), 2); | |
| 172 | 172 | ||
| 173 | 173 | // Source that doesn't exist | |
| 174 | 174 | let gen2 = FeedGenerator::new(db).with_filter(FeedFilter::new().source("nonexistent")); | |
| @@ -185,6 +185,6 @@ async fn generator_count_with_unread_filter() { | |||
| 185 | 185 | ||
| 186 | 186 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 187 | 187 | ||
| 188 | - | let gen = FeedGenerator::new(db).with_filter(FeedFilter::new().unread_only()); | |
| 189 | - | assert_eq!(gen.count().await.unwrap(), 2); | |
| 188 | + | let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().unread_only()); | |
| 189 | + | assert_eq!(fg.count().await.unwrap(), 2); | |
| 190 | 190 | } |
| @@ -296,10 +296,10 @@ async fn source_listing_unread_counts_update_after_mark_read() { | |||
| 296 | 296 | common::insert_item(&db, &feed_rss, "rss:2", "RSS 2", 2).await; | |
| 297 | 297 | common::insert_item(&db, &feed_hn, "hn:1", "HN 1", 1).await; | |
| 298 | 298 | ||
| 299 | - | let gen = FeedGenerator::new(db.clone()); | |
| 299 | + | let fg = FeedGenerator::new(db.clone()); | |
| 300 | 300 | ||
| 301 | 301 | // Initial state: RSS has 2 unread, HN has 1 unread | |
| 302 | - | let sources = gen.get_sources().await.unwrap(); | |
| 302 | + | let sources = fg.get_sources().await.unwrap(); | |
| 303 | 303 | let rss = sources.iter().find(|s| s.id == "rss").unwrap(); | |
| 304 | 304 | assert_eq!(rss.unread_count, 2); | |
| 305 | 305 | let hn = sources.iter().find(|s| s.id == "hn").unwrap(); | |
| @@ -308,7 +308,7 @@ async fn source_listing_unread_counts_update_after_mark_read() { | |||
| 308 | 308 | // Mark one RSS item read | |
| 309 | 309 | db.items().mark_read(rss1.id, true).await.unwrap(); | |
| 310 | 310 | ||
| 311 | - | let sources = gen.get_sources().await.unwrap(); | |
| 311 | + | let sources = fg.get_sources().await.unwrap(); | |
| 312 | 312 | let rss = sources.iter().find(|s| s.id == "rss").unwrap(); | |
| 313 | 313 | assert_eq!(rss.unread_count, 1); | |
| 314 | 314 | assert_eq!(rss.total_count, 2); // total unchanged | |
| @@ -337,8 +337,8 @@ async fn source_health_circuit_broken_overrides_failure_count() { | |||
| 337 | 337 | assert!(refreshed.circuit_broken); | |
| 338 | 338 | ||
| 339 | 339 | // Replicate the health mapping from sources.rs | |
| 340 | - | let gen = FeedGenerator::new(db.clone()); | |
| 341 | - | let sources = gen.get_sources().await.unwrap(); | |
| 340 | + | let fg = FeedGenerator::new(db.clone()); | |
| 341 | + | let sources = fg.get_sources().await.unwrap(); | |
| 342 | 342 | let source = &sources[0]; | |
| 343 | 343 | ||
| 344 | 344 | // The sources.rs command checks circuit_broken first, before failure count | |
| @@ -372,8 +372,8 @@ async fn source_health_circuit_broken_resets_to_green() { | |||
| 372 | 372 | // Reset via the repository method (mirrors reset_circuit_breaker command) | |
| 373 | 373 | db.feeds().reset_circuit_breaker(feed.id).await.unwrap(); | |
| 374 | 374 | ||
| 375 | - | let gen = FeedGenerator::new(db.clone()); | |
| 376 | - | let sources = gen.get_sources().await.unwrap(); | |
| 375 | + | let fg = FeedGenerator::new(db.clone()); | |
| 376 | + | let sources = fg.get_sources().await.unwrap(); | |
| 377 | 377 | let source = &sources[0]; | |
| 378 | 378 | ||
| 379 | 379 | assert!(!source.circuit_broken); | |
| @@ -409,8 +409,8 @@ async fn source_listing_includes_feed_tags() { | |||
| 409 | 409 | .await | |
| 410 | 410 | .unwrap(); | |
| 411 | 411 | ||
| 412 | - | let gen = FeedGenerator::new(db); | |
| 413 | - | let sources = gen.get_sources().await.unwrap(); | |
| 412 | + | let fg = FeedGenerator::new(db); | |
| 413 | + | let sources = fg.get_sources().await.unwrap(); | |
| 414 | 414 | ||
| 415 | 415 | let rss = sources.iter().find(|s| s.id == "rss").unwrap(); | |
| 416 | 416 | assert!(rss.tags.contains(&"tech".to_string())); | |
| @@ -428,8 +428,8 @@ async fn source_listing_feed_with_no_items() { | |||
| 428 | 428 | common::create_rss_feed(&db, "Empty RSS", "https://example.com/rss").await; | |
| 429 | 429 | common::create_other_feed(&db, "hn", "Empty HN").await; | |
| 430 | 430 | ||
| 431 | - | let gen = FeedGenerator::new(db); | |
| 432 | - | let sources = gen.get_sources().await.unwrap(); | |
| 431 | + | let fg = FeedGenerator::new(db); | |
| 432 | + | let sources = fg.get_sources().await.unwrap(); | |
| 433 | 433 | assert_eq!(sources.len(), 2); | |
| 434 | 434 | ||
| 435 | 435 | for source in &sources { |
| @@ -115,16 +115,16 @@ async fn list_items_pagination() { | |||
| 115 | 115 | common::insert_item(&db, &feed, &format!("rss:p{i}"), &format!("Item {i}"), i).await; | |
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | - | let gen = FeedGenerator::new(db).with_page_size(3); | |
| 119 | - | let page0 = gen.get_items(0).await.unwrap(); | |
| 118 | + | let fg = FeedGenerator::new(db).with_page_size(3); | |
| 119 | + | let page0 = fg.get_items(0).await.unwrap(); | |
| 120 | 120 | assert_eq!(page0.items.len(), 3); | |
| 121 | 121 | assert!(page0.has_more); | |
| 122 | 122 | ||
| 123 | - | let page1 = gen.get_items(1).await.unwrap(); | |
| 123 | + | let page1 = fg.get_items(1).await.unwrap(); | |
| 124 | 124 | assert_eq!(page1.items.len(), 3); | |
| 125 | 125 | assert!(page1.has_more); | |
| 126 | 126 | ||
| 127 | - | let page2 = gen.get_items(2).await.unwrap(); | |
| 127 | + | let page2 = fg.get_items(2).await.unwrap(); | |
| 128 | 128 | assert_eq!(page2.items.len(), 1); | |
| 129 | 129 | assert!(!page2.has_more); | |
| 130 | 130 | } | |
| @@ -138,8 +138,8 @@ async fn list_items_with_source_filter() { | |||
| 138 | 138 | common::insert_item(&db, &feed_hn, "hn:1", "HN Article", 1).await; | |
| 139 | 139 | common::insert_item(&db, &feed_hn, "hn:2", "HN Article 2", 2).await; | |
| 140 | 140 | ||
| 141 | - | let gen = FeedGenerator::new(db).with_filter(FeedFilter::new().source("hn")); | |
| 142 | - | let result = gen.get_items(0).await.unwrap(); | |
| 141 | + | let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().source("hn")); | |
| 142 | + | let result = fg.get_items(0).await.unwrap(); | |
| 143 | 143 | assert_eq!(result.items.len(), 2); | |
| 144 | 144 | for item in &result.items { | |
| 145 | 145 | assert_eq!(item.id.source, "hn"); | |
| @@ -155,8 +155,8 @@ async fn list_items_unread_only() { | |||
| 155 | 155 | ||
| 156 | 156 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 157 | 157 | ||
| 158 | - | let gen = FeedGenerator::new(db).with_filter(FeedFilter::new().unread_only()); | |
| 159 | - | let result = gen.get_items(0).await.unwrap(); | |
| 158 | + | let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().unread_only()); | |
| 159 | + | let result = fg.get_items(0).await.unwrap(); | |
| 160 | 160 | assert_eq!(result.items.len(), 1); | |
| 161 | 161 | assert!(!result.items[0].is_read); | |
| 162 | 162 | } | |
| @@ -170,8 +170,8 @@ async fn list_items_starred_only() { | |||
| 170 | 170 | ||
| 171 | 171 | db.items().mark_starred(item2.id, true).await.unwrap(); | |
| 172 | 172 | ||
| 173 | - | let gen = FeedGenerator::new(db).with_filter(FeedFilter::new().starred_only()); | |
| 174 | - | let result = gen.get_items(0).await.unwrap(); | |
| 173 | + | let fg = FeedGenerator::new(db).with_filter(FeedFilter::new().starred_only()); | |
| 174 | + | let result = fg.get_items(0).await.unwrap(); | |
| 175 | 175 | assert_eq!(result.items.len(), 1); | |
| 176 | 176 | assert!(result.items[0].is_starred); | |
| 177 | 177 | } | |
| @@ -307,25 +307,25 @@ async fn list_items_combined_source_unread_starred() { | |||
| 307 | 307 | ||
| 308 | 308 | // Filter: source=rss — gets both RSS items (source branch doesn't also | |
| 309 | 309 | // filter by unread; only list_search combines source+unread) | |
| 310 | - | let gen = FeedGenerator::new(db.clone()) | |
| 310 | + | let fg = FeedGenerator::new(db.clone()) | |
| 311 | 311 | .with_filter(FeedFilter::new().source("rss")); | |
| 312 | - | let result = gen.get_items(0).await.unwrap(); | |
| 312 | + | let result = fg.get_items(0).await.unwrap(); | |
| 313 | 313 | assert_eq!(result.items.len(), 2); | |
| 314 | 314 | for item in &result.items { | |
| 315 | 315 | assert_eq!(item.id.source, "rss"); | |
| 316 | 316 | } | |
| 317 | 317 | ||
| 318 | 318 | // Filter: starred only — should get rss:2 only | |
| 319 | - | let gen2 = FeedGenerator::new(db.clone()) | |
| 319 | + | let fg2 = FeedGenerator::new(db.clone()) | |
| 320 | 320 | .with_filter(FeedFilter::new().starred_only()); | |
| 321 | - | let result2 = gen2.get_items(0).await.unwrap(); | |
| 321 | + | let result2 = fg2.get_items(0).await.unwrap(); | |
| 322 | 322 | assert_eq!(result2.items.len(), 1); | |
| 323 | 323 | assert!(result2.items[0].is_starred); | |
| 324 | 324 | ||
| 325 | 325 | // Search + source + unread combines all filters in SQL | |
| 326 | - | let gen3 = FeedGenerator::new(db) | |
| 326 | + | let fg3 = FeedGenerator::new(db) | |
| 327 | 327 | .with_filter(FeedFilter::new().search("RSS").source("rss").unread_only()); | |
| 328 | - | let result3 = gen3.get_items(0).await.unwrap(); | |
| 328 | + | let result3 = fg3.get_items(0).await.unwrap(); | |
| 329 | 329 | assert_eq!(result3.items.len(), 1); | |
| 330 | 330 | assert!(!result3.items[0].is_read); | |
| 331 | 331 | } | |
| @@ -335,8 +335,8 @@ async fn list_items_combined_source_unread_starred() { | |||
| 335 | 335 | #[tokio::test] | |
| 336 | 336 | async fn list_items_empty_database() { | |
| 337 | 337 | let db = common::test_db().await; | |
| 338 | - | let gen = FeedGenerator::new(db); | |
| 339 | - | let result = gen.get_items(0).await.unwrap(); | |
| 338 | + | let fg = FeedGenerator::new(db); | |
| 339 | + | let result = fg.get_items(0).await.unwrap(); | |
| 340 | 340 | assert!(result.items.is_empty()); | |
| 341 | 341 | assert!(!result.has_more); | |
| 342 | 342 | } | |
| @@ -344,8 +344,8 @@ async fn list_items_empty_database() { | |||
| 344 | 344 | #[tokio::test] | |
| 345 | 345 | async fn list_sources_empty_database() { | |
| 346 | 346 | let db = common::test_db().await; | |
| 347 | - | let gen = FeedGenerator::new(db); | |
| 348 | - | let sources = gen.get_sources().await.unwrap(); | |
| 347 | + | let fg = FeedGenerator::new(db); | |
| 348 | + | let sources = fg.get_sources().await.unwrap(); | |
| 349 | 349 | assert!(sources.is_empty()); | |
| 350 | 350 | } | |
| 351 | 351 | ||
| @@ -354,15 +354,15 @@ async fn unread_count_empty_database() { | |||
| 354 | 354 | let db = common::test_db().await; | |
| 355 | 355 | assert_eq!(db.items().count_unread().await.unwrap(), 0); | |
| 356 | 356 | ||
| 357 | - | let gen = FeedGenerator::new(db); | |
| 358 | - | assert_eq!(gen.unread_count().await.unwrap(), 0); | |
| 357 | + | let fg = FeedGenerator::new(db); | |
| 358 | + | assert_eq!(fg.unread_count().await.unwrap(), 0); | |
| 359 | 359 | } | |
| 360 | 360 | ||
| 361 | 361 | #[tokio::test] | |
| 362 | 362 | async fn count_empty_database() { | |
| 363 | 363 | let db = common::test_db().await; | |
| 364 | - | let gen = FeedGenerator::new(db); | |
| 365 | - | assert_eq!(gen.count().await.unwrap(), 0); | |
| 364 | + | let fg = FeedGenerator::new(db); | |
| 365 | + | assert_eq!(fg.count().await.unwrap(), 0); | |
| 366 | 366 | } | |
| 367 | 367 | ||
| 368 | 368 | // ── Mark Read/Star: Verify Persisted State in Filtered Queries ─────── | |
| @@ -375,19 +375,19 @@ async fn mark_read_excludes_from_unread_filter() { | |||
| 375 | 375 | common::insert_item(&db, &feed, "rss:ex2", "Article 2", 2).await; | |
| 376 | 376 | common::insert_item(&db, &feed, "rss:ex3", "Article 3", 3).await; | |
| 377 | 377 | ||
| 378 | - | let gen = FeedGenerator::new(db.clone()) | |
| 378 | + | let fg = FeedGenerator::new(db.clone()) | |
| 379 | 379 | .with_filter(FeedFilter::new().unread_only()); | |
| 380 | 380 | ||
| 381 | 381 | // All 3 visible | |
| 382 | - | assert_eq!(gen.get_items(0).await.unwrap().items.len(), 3); | |
| 382 | + | assert_eq!(fg.get_items(0).await.unwrap().items.len(), 3); | |
| 383 | 383 | ||
| 384 | 384 | // Mark one read | |
| 385 | 385 | db.items().mark_read(item1.id, true).await.unwrap(); | |
| 386 | 386 | ||
| 387 | 387 | // Only 2 visible now | |
| 388 | - | assert_eq!(gen.get_items(0).await.unwrap().items.len(), 2); | |
| 388 | + | assert_eq!(fg.get_items(0).await.unwrap().items.len(), 2); | |
| 389 | 389 | // None of the returned items should be the read one | |
| 390 | - | let ids: Vec<String> = gen | |
| 390 | + | let ids: Vec<String> = fg | |
| 391 | 391 | .get_items(0) | |
| 392 | 392 | .await | |
| 393 | 393 | .unwrap() | |
| @@ -406,23 +406,23 @@ async fn star_item_appears_in_starred_filter() { | |||
| 406 | 406 | let item2 = common::insert_item(&db, &feed, "rss:st2", "Article 2", 2).await; | |
| 407 | 407 | common::insert_item(&db, &feed, "rss:st3", "Article 3", 3).await; | |
| 408 | 408 | ||
| 409 | - | let gen_starred = FeedGenerator::new(db.clone()) | |
| 409 | + | let fg_starred = FeedGenerator::new(db.clone()) | |
| 410 | 410 | .with_filter(FeedFilter::new().starred_only()); | |
| 411 | 411 | ||
| 412 | 412 | // Nothing starred yet | |
| 413 | - | assert!(gen_starred.get_items(0).await.unwrap().items.is_empty()); | |
| 413 | + | assert!(fg_starred.get_items(0).await.unwrap().items.is_empty()); | |
| 414 | 414 | ||
| 415 | 415 | // Star one item | |
| 416 | 416 | db.items().mark_starred(item2.id, true).await.unwrap(); | |
| 417 | 417 | ||
| 418 | - | let result = gen_starred.get_items(0).await.unwrap(); | |
| 418 | + | let result = fg_starred.get_items(0).await.unwrap(); | |
| 419 | 419 | assert_eq!(result.items.len(), 1); | |
| 420 | 420 | assert_eq!(result.items[0].id.item_id, "rss:st2"); | |
| 421 | 421 | assert!(result.items[0].is_starred); | |
| 422 | 422 | ||
| 423 | 423 | // Unstar it | |
| 424 | 424 | db.items().mark_starred(item2.id, false).await.unwrap(); | |
| 425 | - | assert!(gen_starred.get_items(0).await.unwrap().items.is_empty()); | |
| 425 | + | assert!(fg_starred.get_items(0).await.unwrap().items.is_empty()); | |
| 426 | 426 | } | |
| 427 | 427 | ||
| 428 | 428 | // ── Upsert Preserves User State ────────────────────────────────────── |
| @@ -92,8 +92,8 @@ async fn orchestrator_source_listing() { | |||
| 92 | 92 | common::insert_item(db, &feed_hn, "hn:s2", "HN Item 2", 2).await; | |
| 93 | 93 | ||
| 94 | 94 | // Replicate list_sources command logic | |
| 95 | - | let gen = FeedGenerator::new(db.clone()); | |
| 96 | - | let sources = gen.get_sources().await.unwrap(); | |
| 95 | + | let fg = FeedGenerator::new(db.clone()); | |
| 96 | + | let sources = fg.get_sources().await.unwrap(); | |
| 97 | 97 | assert_eq!(sources.len(), 2); | |
| 98 | 98 | ||
| 99 | 99 | let hn = sources.iter().find(|s| s.id == "hn").unwrap(); | |
| @@ -113,8 +113,8 @@ async fn source_health_green_on_no_failures() { | |||
| 113 | 113 | let feed = common::create_rss_feed(&db, "Feed", "https://example.com/rss").await; | |
| 114 | 114 | common::insert_item(&db, &feed, "rss:1", "Item", 1).await; | |
| 115 | 115 | ||
| 116 | - | let gen = FeedGenerator::new(db); | |
| 117 | - | let sources = gen.get_sources().await.unwrap(); | |
| 116 | + | let fg = FeedGenerator::new(db); | |
| 117 | + | let sources = fg.get_sources().await.unwrap(); | |
| 118 | 118 | assert_eq!(sources[0].consecutive_failures, 0); | |
| 119 | 119 | ||
| 120 | 120 | // Replicate health mapping from sources.rs command | |
| @@ -136,8 +136,8 @@ async fn source_health_yellow_on_few_failures() { | |||
| 136 | 136 | .await | |
| 137 | 137 | .unwrap(); | |
| 138 | 138 | ||
| 139 | - | let gen = FeedGenerator::new(db); | |
| 140 | - | let sources = gen.get_sources().await.unwrap(); | |
| 139 | + | let fg = FeedGenerator::new(db); | |
| 140 | + | let sources = fg.get_sources().await.unwrap(); | |
| 141 | 141 | let health = match sources[0].consecutive_failures { | |
| 142 | 142 | 0 => "green", | |
| 143 | 143 | 1..=2 => "yellow", | |
| @@ -158,8 +158,8 @@ async fn source_health_red_on_many_failures() { | |||
| 158 | 158 | .unwrap(); | |
| 159 | 159 | } | |
| 160 | 160 | ||
| 161 | - | let gen = FeedGenerator::new(db); | |
| 162 | - | let sources = gen.get_sources().await.unwrap(); | |
| 161 | + | let fg = FeedGenerator::new(db); | |
| 162 | + | let sources = fg.get_sources().await.unwrap(); | |
| 163 | 163 | let health = match sources[0].consecutive_failures { | |
| 164 | 164 | 0 => "green", | |
| 165 | 165 | 1..=2 => "yellow", | |
| @@ -191,8 +191,8 @@ async fn source_health_resets_after_success() { | |||
| 191 | 191 | .await | |
| 192 | 192 | .unwrap(); | |
| 193 | 193 | ||
| 194 | - | let gen = FeedGenerator::new(db); | |
| 195 | - | let sources = gen.get_sources().await.unwrap(); | |
| 194 | + | let fg = FeedGenerator::new(db); | |
| 195 | + | let sources = fg.get_sources().await.unwrap(); | |
| 196 | 196 | assert_eq!(sources[0].consecutive_failures, 0); | |
| 197 | 197 | assert!(sources[0].last_error.is_none()); | |
| 198 | 198 | } |