feat: add max_articles_per_source setting (migration + model + DB)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
oabrivard 3 months ago
parent a3f4c3b42f
commit c1ee79bcf6

@ -117,7 +117,7 @@ cd frontend && npx tsc --noEmit
- `GET /api/v1/admin/users` — user list - `GET /api/v1/admin/users` — user list
- `PUT /api/v1/admin/users/:id/role` — role management - `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` Tables: `users`, `sessions`, `magic_link_tokens`, `user_settings`, `sources`, `syntheses`, `admin_providers`, `admin_rate_limits`, `user_api_keys`, `audit_log`
## Environment Variables ## Environment Variables

@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN max_articles_per_source INTEGER NOT NULL DEFAULT 3;

@ -17,6 +17,7 @@ struct SettingsRow {
max_age_days: i32, max_age_days: i32,
categories: serde_json::Value, categories: serde_json::Value,
max_items_per_category: i32, max_items_per_category: i32,
max_articles_per_source: i32,
search_agent_behavior: String, search_agent_behavior: String,
ai_provider: String, ai_provider: String,
ai_model: String, ai_model: String,
@ -40,6 +41,7 @@ impl TryFrom<SettingsRow> for UserSettings {
max_age_days: row.max_age_days, max_age_days: row.max_age_days,
categories, categories,
max_items_per_category: row.max_items_per_category, 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, search_agent_behavior: row.search_agent_behavior,
ai_provider: row.ai_provider, ai_provider: row.ai_provider,
ai_model: row.ai_model, ai_model: row.ai_model,
@ -66,10 +68,10 @@ pub async fn get_or_create_default(
let row = sqlx::query_as::<_, SettingsRow>( let row = sqlx::query_as::<_, SettingsRow>(
r#" 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) 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) 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 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) .bind(user_id)
@ -83,6 +85,7 @@ pub async fn get_or_create_default(
.bind(&defaults.ai_model_writing) .bind(&defaults.ai_model_writing)
.bind(defaults.rate_limit_max_requests) .bind(defaults.rate_limit_max_requests)
.bind(defaults.rate_limit_time_window_seconds) .bind(defaults.rate_limit_time_window_seconds)
.bind(defaults.max_articles_per_source)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
@ -101,8 +104,8 @@ pub async fn upsert(
let row = sqlx::query_as::<_, SettingsRow>( let row = sqlx::query_as::<_, SettingsRow>(
r#" 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) 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (user_id) DO UPDATE SET ON CONFLICT (user_id) DO UPDATE SET
theme = EXCLUDED.theme, theme = EXCLUDED.theme,
max_age_days = EXCLUDED.max_age_days, max_age_days = EXCLUDED.max_age_days,
@ -114,8 +117,9 @@ pub async fn upsert(
ai_model_writing = EXCLUDED.ai_model_writing, ai_model_writing = EXCLUDED.ai_model_writing,
rate_limit_max_requests = EXCLUDED.rate_limit_max_requests, rate_limit_max_requests = EXCLUDED.rate_limit_max_requests,
rate_limit_time_window_seconds = EXCLUDED.rate_limit_time_window_seconds, rate_limit_time_window_seconds = EXCLUDED.rate_limit_time_window_seconds,
max_articles_per_source = EXCLUDED.max_articles_per_source,
updated_at = now() 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) .bind(user_id)
@ -129,6 +133,7 @@ pub async fn upsert(
.bind(&req.ai_model_writing) .bind(&req.ai_model_writing)
.bind(req.rate_limit_max_requests) .bind(req.rate_limit_max_requests)
.bind(req.rate_limit_time_window_seconds) .bind(req.rate_limit_time_window_seconds)
.bind(req.max_articles_per_source)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;

@ -12,6 +12,7 @@ pub struct UserSettings {
pub max_age_days: i32, pub max_age_days: i32,
pub categories: Vec<String>, pub categories: Vec<String>,
pub max_items_per_category: i32, pub max_items_per_category: i32,
pub max_articles_per_source: i32,
pub search_agent_behavior: String, pub search_agent_behavior: String,
pub ai_provider: String, pub ai_provider: String,
pub ai_model: String, pub ai_model: String,
@ -28,6 +29,7 @@ pub struct SettingsResponse {
pub max_age_days: i32, pub max_age_days: i32,
pub categories: Vec<String>, pub categories: Vec<String>,
pub max_items_per_category: i32, pub max_items_per_category: i32,
pub max_articles_per_source: i32,
pub search_agent_behavior: String, pub search_agent_behavior: String,
pub ai_provider: String, pub ai_provider: String,
pub ai_model: String, pub ai_model: String,
@ -43,6 +45,7 @@ impl From<UserSettings> for SettingsResponse {
max_age_days: s.max_age_days, max_age_days: s.max_age_days,
categories: s.categories, categories: s.categories,
max_items_per_category: s.max_items_per_category, 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, search_agent_behavior: s.search_agent_behavior,
ai_provider: s.ai_provider, ai_provider: s.ai_provider,
ai_model: s.ai_model, ai_model: s.ai_model,
@ -60,6 +63,7 @@ pub struct UpdateSettingsRequest {
pub max_age_days: i32, pub max_age_days: i32,
pub categories: Vec<String>, pub categories: Vec<String>,
pub max_items_per_category: i32, pub max_items_per_category: i32,
pub max_articles_per_source: i32,
pub search_agent_behavior: String, pub search_agent_behavior: String,
pub ai_provider: String, pub ai_provider: String,
pub ai_model: String, pub ai_model: String,
@ -103,6 +107,9 @@ impl UpdateSettingsRequest {
if !(1..=50).contains(&self.max_items_per_category) { if !(1..=50).contains(&self.max_items_per_category) {
return Err("max_items_per_category must be between 1 and 50".into()); 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 { if self.search_agent_behavior.len() > 2000 {
return Err("search_agent_behavior must be at most 2000 characters".into()); 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(), "Opinions et analyses".to_string(),
], ],
max_items_per_category: 4, max_items_per_category: 4,
max_articles_per_source: 3,
search_agent_behavior: String::new(), search_agent_behavior: String::new(),
ai_provider: String::new(), ai_provider: String::new(),
ai_model: String::new(), ai_model: String::new(),
@ -166,6 +174,7 @@ mod tests {
max_age_days: 7, max_age_days: 7,
categories: vec!["Category 1".into(), "Category 2".into()], categories: vec!["Category 1".into(), "Category 2".into()],
max_items_per_category: 4, max_items_per_category: 4,
max_articles_per_source: 3,
search_agent_behavior: String::new(), search_agent_behavior: String::new(),
ai_provider: String::new(), ai_provider: String::new(),
ai_model: String::new(), ai_model: String::new(),

@ -144,6 +144,7 @@ mod tests {
"Recherche et innovation".to_string(), "Recherche et innovation".to_string(),
], ],
max_items_per_category: 4, max_items_per_category: 4,
max_articles_per_source: 3,
search_agent_behavior: String::new(), search_agent_behavior: String::new(),
ai_provider: String::new(), ai_provider: String::new(),
ai_model: String::new(), ai_model: String::new(),

Loading…
Cancel
Save