use serde::{Deserialize, Serialize};
use sqlx::{Row, SqlitePool};
use crate::error::Result;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "lowercase")]
pub enum SettingsSection {
Network,
Agents,
Inbox,
Advanced,
}
impl SettingsSection {
fn as_str(self) -> &'static str {
match self {
SettingsSection::Network => "network",
SettingsSection::Agents => "agents",
SettingsSection::Inbox => "inbox",
SettingsSection::Advanced => "advanced",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase")]
pub struct NetworkPrefs {
pub smtp_port: u16,
pub http_api_port: u16,
pub mcp_enabled: bool,
pub mcp_port: u16,
pub expose_on_lan: bool,
#[serde(default)]
pub api_tls: bool,
#[serde(default)]
pub api_auth_token: Option<String>,
}
impl Default for NetworkPrefs {
fn default() -> Self {
Self {
smtp_port: 1025,
http_api_port: 1080,
mcp_enabled: true,
mcp_port: 1081,
expose_on_lan: false,
api_tls: false,
api_auth_token: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase")]
pub struct AgentPrefs {
pub default_wait_timeout_seconds: u32,
pub log_agent_requests: bool,
pub confirm_destructive_actions: bool,
}
impl Default for AgentPrefs {
fn default() -> Self {
Self {
default_wait_timeout_seconds: 30,
log_agent_requests: true,
confirm_destructive_actions: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase")]
pub struct InboxPrefs {
pub max_retained_emails: u32,
pub auto_clear_after_days: u32,
pub thread_related: bool,
pub auto_tag: bool,
}
impl Default for InboxPrefs {
fn default() -> Self {
Self {
max_retained_emails: 5000,
auto_clear_after_days: 14,
thread_related: true,
auto_tag: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase")]
pub struct AdvancedPrefs {
pub debug_logging: bool,
pub preserve_smtp_transcript: bool,
pub audit_retain_days: u32,
}
impl Default for AdvancedPrefs {
fn default() -> Self {
Self {
debug_logging: false,
preserve_smtp_transcript: true,
audit_retain_days: 90,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase")]
pub struct BackendSettings {
pub network: NetworkPrefs,
pub agents: AgentPrefs,
pub inbox: InboxPrefs,
pub advanced: AdvancedPrefs,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[serde(rename_all = "camelCase", tag = "section", content = "value")]
pub enum SettingsPatch {
Network(NetworkPrefs),
Agents(AgentPrefs),
Inbox(InboxPrefs),
Advanced(AdvancedPrefs),
}
impl SettingsPatch {
pub(crate) fn section(&self) -> SettingsSection {
match self {
SettingsPatch::Network(_) => SettingsSection::Network,
SettingsPatch::Agents(_) => SettingsSection::Agents,
SettingsPatch::Inbox(_) => SettingsSection::Inbox,
SettingsPatch::Advanced(_) => SettingsSection::Advanced,
}
}
}
pub(crate) async fn get_section_raw(
pool: &SqlitePool,
section: SettingsSection,
) -> Result<serde_json::Value> {
let rows = sqlx::query("SELECT key, value FROM settings WHERE section = ?")
.bind(section.as_str())
.fetch_all(pool)
.await?;
let mut map = serde_json::Map::new();
for r in rows {
let k: String = r.try_get("key").unwrap_or_default();
let v: String = r.try_get("value").unwrap_or_default();
let parsed: serde_json::Value =
serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v));
map.insert(k, parsed);
}
Ok(serde_json::Value::Object(map))
}
pub(crate) async fn save_section(
pool: &SqlitePool,
section: SettingsSection,
value: serde_json::Value,
) -> Result<()> {
let serde_json::Value::Object(map) = value else {
let mut tx = pool.begin().await?;
sqlx::query("DELETE FROM settings WHERE section = ?")
.bind(section.as_str())
.execute(&mut *tx)
.await?;
sqlx::query(
r"INSERT INTO settings (section, key, value)
VALUES (?, ?, ?)",
)
.bind(section.as_str())
.bind("_value")
.bind(serde_json::to_string(&value).unwrap_or_else(|_| "null".into()))
.execute(&mut *tx)
.await?;
tx.commit().await?;
return Ok(());
};
let mut tx = pool.begin().await?;
sqlx::query("DELETE FROM settings WHERE section = ?")
.bind(section.as_str())
.execute(&mut *tx)
.await?;
for (k, v) in map {
sqlx::query(
r"INSERT INTO settings (section, key, value)
VALUES (?, ?, ?)",
)
.bind(section.as_str())
.bind(&k)
.bind(serde_json::to_string(&v).unwrap_or_else(|_| "null".into()))
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
pub(crate) async fn load_all(pool: &SqlitePool) -> Result<BackendSettings> {
let network = match get_section_raw(pool, SettingsSection::Network).await? {
serde_json::Value::Object(m) if !m.is_empty() => {
serde_json::from_value(serde_json::Value::Object(m)).unwrap_or_default()
}
_ => NetworkPrefs::default(),
};
let agents = match get_section_raw(pool, SettingsSection::Agents).await? {
serde_json::Value::Object(m) if !m.is_empty() => {
serde_json::from_value(serde_json::Value::Object(m)).unwrap_or_default()
}
_ => AgentPrefs::default(),
};
let inbox = match get_section_raw(pool, SettingsSection::Inbox).await? {
serde_json::Value::Object(m) if !m.is_empty() => {
serde_json::from_value(serde_json::Value::Object(m)).unwrap_or_default()
}
_ => InboxPrefs::default(),
};
let advanced = match get_section_raw(pool, SettingsSection::Advanced).await? {
serde_json::Value::Object(m) if !m.is_empty() => {
serde_json::from_value(serde_json::Value::Object(m)).unwrap_or_default()
}
_ => AdvancedPrefs::default(),
};
Ok(BackendSettings {
network,
agents,
inbox,
advanced,
})
}
pub(crate) async fn apply_patch(pool: &SqlitePool, patch: &SettingsPatch) -> Result<()> {
match patch {
SettingsPatch::Network(v) => {
save_section(pool, SettingsSection::Network, serde_json::to_value(v)?).await
}
SettingsPatch::Agents(v) => {
save_section(pool, SettingsSection::Agents, serde_json::to_value(v)?).await
}
SettingsPatch::Inbox(v) => {
save_section(pool, SettingsSection::Inbox, serde_json::to_value(v)?).await
}
SettingsPatch::Advanced(v) => {
save_section(pool, SettingsSection::Advanced, serde_json::to_value(v)?).await
}
}
}