feat: add article_history table and article_history_days setting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent d7c91c956f
commit c271c240a2

@ -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 (14 migrations)
## Database (15 migrations)
Tables: `users`, `sessions`, `magic_link_tokens`, `user_settings`, `sources`, `syntheses`, `admin_providers`, `admin_rate_limits`, `user_api_keys`, `audit_log`
## Environment Variables

@ -0,0 +1,12 @@
-- Article history table for cross-synthesis URL deduplication
CREATE TABLE article_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
url_hash TEXT NOT NULL,
url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_article_history_user_url ON article_history(user_id, url_hash);
-- Setting for history TTL
ALTER TABLE settings ADD COLUMN article_history_days INTEGER NOT NULL DEFAULT 90;

@ -21,6 +21,7 @@ struct SettingsRow {
source_diversity_window: i32,
use_llm_for_source_links: bool,
use_llm_for_article_extraction: bool,
article_history_days: i32,
search_agent_behavior: String,
ai_provider: String,
ai_model: String,
@ -48,6 +49,7 @@ impl TryFrom<SettingsRow> for UserSettings {
source_diversity_window: row.source_diversity_window,
use_llm_for_source_links: row.use_llm_for_source_links,
use_llm_for_article_extraction: row.use_llm_for_article_extraction,
article_history_days: row.article_history_days,
search_agent_behavior: row.search_agent_behavior,
ai_provider: row.ai_provider,
ai_model: row.ai_model,
@ -74,10 +76,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, max_articles_per_source, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
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, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction, article_history_days)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
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, max_articles_per_source, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction, 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, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction, article_history_days, updated_at
"#,
)
.bind(user_id)
@ -95,6 +97,7 @@ pub async fn get_or_create_default(
.bind(defaults.source_diversity_window)
.bind(defaults.use_llm_for_source_links)
.bind(defaults.use_llm_for_article_extraction)
.bind(defaults.article_history_days)
.fetch_one(pool)
.await?;
@ -113,8 +116,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, max_articles_per_source, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
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, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction, article_history_days)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (user_id) DO UPDATE SET
theme = EXCLUDED.theme,
max_age_days = EXCLUDED.max_age_days,
@ -130,8 +133,9 @@ pub async fn upsert(
source_diversity_window = EXCLUDED.source_diversity_window,
use_llm_for_source_links = EXCLUDED.use_llm_for_source_links,
use_llm_for_article_extraction = EXCLUDED.use_llm_for_article_extraction,
article_history_days = EXCLUDED.article_history_days,
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, max_articles_per_source, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction, 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, source_diversity_window, use_llm_for_source_links, use_llm_for_article_extraction, article_history_days, updated_at
"#,
)
.bind(user_id)
@ -149,6 +153,7 @@ pub async fn upsert(
.bind(req.source_diversity_window)
.bind(req.use_llm_for_source_links)
.bind(req.use_llm_for_article_extraction)
.bind(req.article_history_days)
.fetch_one(pool)
.await?;

@ -16,6 +16,7 @@ pub struct UserSettings {
pub source_diversity_window: i32,
pub use_llm_for_source_links: bool,
pub use_llm_for_article_extraction: bool,
pub article_history_days: i32,
pub search_agent_behavior: String,
pub ai_provider: String,
pub ai_model: String,
@ -36,6 +37,7 @@ pub struct SettingsResponse {
pub source_diversity_window: i32,
pub use_llm_for_source_links: bool,
pub use_llm_for_article_extraction: bool,
pub article_history_days: i32,
pub search_agent_behavior: String,
pub ai_provider: String,
pub ai_model: String,
@ -55,6 +57,7 @@ impl From<UserSettings> for SettingsResponse {
source_diversity_window: s.source_diversity_window,
use_llm_for_source_links: s.use_llm_for_source_links,
use_llm_for_article_extraction: s.use_llm_for_article_extraction,
article_history_days: s.article_history_days,
search_agent_behavior: s.search_agent_behavior,
ai_provider: s.ai_provider,
ai_model: s.ai_model,
@ -76,6 +79,7 @@ pub struct UpdateSettingsRequest {
pub source_diversity_window: i32,
pub use_llm_for_source_links: bool,
pub use_llm_for_article_extraction: bool,
pub article_history_days: i32,
pub search_agent_behavior: String,
pub ai_provider: String,
pub ai_model: String,
@ -125,6 +129,9 @@ impl UpdateSettingsRequest {
if !(0..=10).contains(&self.source_diversity_window) {
return Err("source_diversity_window must be between 0 and 10".into());
}
if !(0..=365).contains(&self.article_history_days) {
return Err("article_history_days must be between 0 and 365".into());
}
if self.search_agent_behavior.len() > 2000 {
return Err("search_agent_behavior must be at most 2000 characters".into());
}
@ -170,6 +177,7 @@ impl Default for UserSettings {
source_diversity_window: 3,
use_llm_for_source_links: false,
use_llm_for_article_extraction: false,
article_history_days: 90,
search_agent_behavior: String::new(),
ai_provider: String::new(),
ai_model: String::new(),
@ -196,6 +204,7 @@ mod tests {
source_diversity_window: 3,
use_llm_for_source_links: false,
use_llm_for_article_extraction: false,
article_history_days: 90,
search_agent_behavior: String::new(),
ai_provider: String::new(),
ai_model: String::new(),

@ -283,6 +283,7 @@ mod tests {
source_diversity_window: 3,
use_llm_for_source_links: false,
use_llm_for_article_extraction: false,
article_history_days: 90,
search_agent_behavior: String::new(),
ai_provider: String::new(),
ai_model: String::new(),

Loading…
Cancel
Save