From c1ee79bcf68dd3df66aa25ac18bf38005564357c Mon Sep 17 00:00:00 2001 From: oabrivard Date: Mon, 23 Mar 2026 21:09:16 +0100 Subject: [PATCH] feat: add max_articles_per_source setting (migration + model + DB) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 +- ...260323000012_add_max_articles_per_source.sql | 1 + backend/src/db/settings.rs | 17 +++++++++++------ backend/src/models/settings.rs | 9 +++++++++ backend/src/services/prompts.rs | 1 + 5 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 backend/migrations/20260323000012_add_max_articles_per_source.sql diff --git a/CLAUDE.md b/CLAUDE.md index a0694ba..e3011f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,7 @@ cd frontend && npx tsc --noEmit - `GET /api/v1/admin/users` — user list - `PUT /api/v1/admin/users/:id/role` — role management -## Database (11 migrations) +## Database (12 migrations) Tables: `users`, `sessions`, `magic_link_tokens`, `user_settings`, `sources`, `syntheses`, `admin_providers`, `admin_rate_limits`, `user_api_keys`, `audit_log` ## Environment Variables diff --git a/backend/migrations/20260323000012_add_max_articles_per_source.sql b/backend/migrations/20260323000012_add_max_articles_per_source.sql new file mode 100644 index 0000000..a1fa2ee --- /dev/null +++ b/backend/migrations/20260323000012_add_max_articles_per_source.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN max_articles_per_source INTEGER NOT NULL DEFAULT 3; diff --git a/backend/src/db/settings.rs b/backend/src/db/settings.rs index 8e9f7b5..72137f4 100644 --- a/backend/src/db/settings.rs +++ b/backend/src/db/settings.rs @@ -17,6 +17,7 @@ struct SettingsRow { max_age_days: i32, categories: serde_json::Value, max_items_per_category: i32, + max_articles_per_source: i32, search_agent_behavior: String, ai_provider: String, ai_model: String, @@ -40,6 +41,7 @@ impl TryFrom for UserSettings { max_age_days: row.max_age_days, categories, max_items_per_category: row.max_items_per_category, + max_articles_per_source: row.max_articles_per_source, search_agent_behavior: row.search_agent_behavior, ai_provider: row.ai_provider, ai_model: row.ai_model, @@ -66,10 +68,10 @@ pub async fn get_or_create_default( let row = sqlx::query_as::<_, SettingsRow>( r#" - INSERT INTO settings (user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + INSERT INTO settings (user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, max_articles_per_source) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (user_id) DO UPDATE SET user_id = settings.user_id - RETURNING user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, updated_at + RETURNING user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, max_articles_per_source, updated_at "#, ) .bind(user_id) @@ -83,6 +85,7 @@ pub async fn get_or_create_default( .bind(&defaults.ai_model_writing) .bind(defaults.rate_limit_max_requests) .bind(defaults.rate_limit_time_window_seconds) + .bind(defaults.max_articles_per_source) .fetch_one(pool) .await?; @@ -101,8 +104,8 @@ pub async fn upsert( let row = sqlx::query_as::<_, SettingsRow>( r#" - INSERT INTO settings (user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + INSERT INTO settings (user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, max_articles_per_source) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme, max_age_days = EXCLUDED.max_age_days, @@ -114,8 +117,9 @@ pub async fn upsert( ai_model_writing = EXCLUDED.ai_model_writing, rate_limit_max_requests = EXCLUDED.rate_limit_max_requests, rate_limit_time_window_seconds = EXCLUDED.rate_limit_time_window_seconds, + max_articles_per_source = EXCLUDED.max_articles_per_source, updated_at = now() - RETURNING user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, updated_at + RETURNING user_id, theme, max_age_days, categories, max_items_per_category, search_agent_behavior, ai_provider, ai_model, ai_model_writing, rate_limit_max_requests, rate_limit_time_window_seconds, max_articles_per_source, updated_at "#, ) .bind(user_id) @@ -129,6 +133,7 @@ pub async fn upsert( .bind(&req.ai_model_writing) .bind(req.rate_limit_max_requests) .bind(req.rate_limit_time_window_seconds) + .bind(req.max_articles_per_source) .fetch_one(pool) .await?; diff --git a/backend/src/models/settings.rs b/backend/src/models/settings.rs index dc7a5f2..386d73d 100644 --- a/backend/src/models/settings.rs +++ b/backend/src/models/settings.rs @@ -12,6 +12,7 @@ pub struct UserSettings { pub max_age_days: i32, pub categories: Vec, pub max_items_per_category: i32, + pub max_articles_per_source: i32, pub search_agent_behavior: String, pub ai_provider: String, pub ai_model: String, @@ -28,6 +29,7 @@ pub struct SettingsResponse { pub max_age_days: i32, pub categories: Vec, pub max_items_per_category: i32, + pub max_articles_per_source: i32, pub search_agent_behavior: String, pub ai_provider: String, pub ai_model: String, @@ -43,6 +45,7 @@ impl From for SettingsResponse { max_age_days: s.max_age_days, categories: s.categories, max_items_per_category: s.max_items_per_category, + max_articles_per_source: s.max_articles_per_source, search_agent_behavior: s.search_agent_behavior, ai_provider: s.ai_provider, ai_model: s.ai_model, @@ -60,6 +63,7 @@ pub struct UpdateSettingsRequest { pub max_age_days: i32, pub categories: Vec, pub max_items_per_category: i32, + pub max_articles_per_source: i32, pub search_agent_behavior: String, pub ai_provider: String, pub ai_model: String, @@ -103,6 +107,9 @@ impl UpdateSettingsRequest { if !(1..=50).contains(&self.max_items_per_category) { return Err("max_items_per_category must be between 1 and 50".into()); } + if !(1..=10).contains(&self.max_articles_per_source) { + return Err("max_articles_per_source must be between 1 and 10".into()); + } if self.search_agent_behavior.len() > 2000 { return Err("search_agent_behavior must be at most 2000 characters".into()); } @@ -144,6 +151,7 @@ impl Default for UserSettings { "Opinions et analyses".to_string(), ], max_items_per_category: 4, + max_articles_per_source: 3, search_agent_behavior: String::new(), ai_provider: String::new(), ai_model: String::new(), @@ -166,6 +174,7 @@ mod tests { max_age_days: 7, categories: vec!["Category 1".into(), "Category 2".into()], max_items_per_category: 4, + max_articles_per_source: 3, search_agent_behavior: String::new(), ai_provider: String::new(), ai_model: String::new(), diff --git a/backend/src/services/prompts.rs b/backend/src/services/prompts.rs index 2ecc471..4df9731 100644 --- a/backend/src/services/prompts.rs +++ b/backend/src/services/prompts.rs @@ -144,6 +144,7 @@ mod tests { "Recherche et innovation".to_string(), ], max_items_per_category: 4, + max_articles_per_source: 3, search_agent_behavior: String::new(), ai_provider: String::new(), ai_model: String::new(),