internal data sources (clock, static text), connection indicator, rendering fixes

DataSourceConfig refactored to enum: External/Clock/StaticText. Clock
generates formatted time via chrono, static text emits configured string.

ESP32: connection status indicator (green/red dot bottom-right), per-widget
clear before redraw, RenderEvent enum for local + server messages.

Polling uses DataUpdate instead of ScreenUpdate to avoid wiping widget state.
Empty mappings passthrough raw source data for internal sources.
This commit is contained in:
2026-06-19 11:26:49 +02:00
parent b448fa15fe
commit a51d22649a
25 changed files with 630 additions and 214 deletions

View File

@@ -18,6 +18,8 @@ pub fn data_source_type_to_str(t: &DataSourceType) -> &'static str {
DataSourceType::Rss => "rss",
DataSourceType::HttpJson => "http_json",
DataSourceType::Webhook => "webhook",
DataSourceType::Clock => "clock",
DataSourceType::StaticText => "static_text",
}
}
@@ -28,6 +30,8 @@ fn data_source_type_from_str(s: &str) -> Result<DataSourceType, SqliteConfigErro
"rss" => Ok(DataSourceType::Rss),
"http_json" => Ok(DataSourceType::HttpJson),
"webhook" => Ok(DataSourceType::Webhook),
"clock" => Ok(DataSourceType::Clock),
"static_text" => Ok(DataSourceType::StaticText),
_ => Err(SqliteConfigError::Serialization(format!(
"unknown source type: {s}"
))),
@@ -38,33 +42,54 @@ pub fn data_source_config_to_json(
config: &DataSourceConfig,
secrets: Option<&(dyn SecretStore + Send + Sync)>,
) -> Result<String, SqliteConfigError> {
let api_key = config.api_key.as_ref().map(|k| match secrets {
Some(s) => s.encrypt(k),
None => k.clone(),
});
let v = match config {
DataSourceConfig::External {
url,
headers,
api_key,
} => {
let api_key = api_key.as_ref().map(|k| match secrets {
Some(s) => s.encrypt(k),
None => k.clone(),
});
let headers: Vec<(String, String)> = config
.headers
.iter()
.map(|(k, v)| {
let val = if is_sensitive_key(k) {
match secrets {
Some(s) => s.encrypt(v),
None => v.clone(),
}
} else {
v.clone()
};
(k.clone(), val)
})
.collect();
let headers: Vec<(String, String)> = headers
.iter()
.map(|(k, v)| {
let val = if is_sensitive_key(k) {
match secrets {
Some(s) => s.encrypt(v),
None => v.clone(),
}
} else {
v.clone()
};
(k.clone(), val)
})
.collect();
let v = serde_json::json!({
"url": config.url,
"headers": headers,
"api_key": api_key,
"encrypted": secrets.is_some(),
});
serde_json::json!({
"type": "external",
"url": url,
"headers": headers,
"api_key": api_key,
"encrypted": secrets.is_some(),
})
}
DataSourceConfig::Clock { format, timezone } => {
serde_json::json!({
"type": "clock",
"format": format,
"timezone": timezone,
})
}
DataSourceConfig::StaticText { text } => {
serde_json::json!({
"type": "static_text",
"text": text,
})
}
};
serde_json::to_string(&v).map_err(|e| SqliteConfigError::Serialization(e.to_string()))
}
@@ -75,47 +100,61 @@ fn data_source_config_from_json(
let v: serde_json::Value =
serde_json::from_str(json).map_err(|e| SqliteConfigError::Serialization(e.to_string()))?;
let encrypted = v["encrypted"].as_bool().unwrap_or(false);
let config_type = v["type"].as_str().unwrap_or("external");
let url = v["url"].as_str().map(String::from);
let api_key = v["api_key"].as_str().map(|k| {
if encrypted {
match secrets {
Some(s) => s.decrypt(k),
None => k.to_string(),
}
} else {
k.to_string()
match config_type {
"clock" => {
let format = v["format"].as_str().unwrap_or("%H:%M:%S").to_string();
let timezone = v["timezone"].as_str().unwrap_or("UTC").to_string();
Ok(DataSourceConfig::Clock { format, timezone })
}
});
"static_text" => {
let text = v["text"].as_str().unwrap_or("").to_string();
Ok(DataSourceConfig::StaticText { text })
}
_ => {
let encrypted = v["encrypted"].as_bool().unwrap_or(false);
let url = v["url"].as_str().map(String::from);
let headers = match v["headers"].as_array() {
Some(arr) => arr
.iter()
.filter_map(|h| {
let pair = h.as_array()?;
let key: String = pair[0].as_str()?.into();
let raw_val: &str = pair[1].as_str()?;
let val = if encrypted && is_sensitive_key(&key) {
let api_key = v["api_key"].as_str().map(|k| {
if encrypted {
match secrets {
Some(s) => s.decrypt(raw_val),
None => raw_val.to_string(),
Some(s) => s.decrypt(k),
None => k.to_string(),
}
} else {
raw_val.to_string()
};
Some((key, val))
})
.collect(),
None => vec![],
};
k.to_string()
}
});
Ok(DataSourceConfig {
url,
headers,
api_key,
})
let headers = match v["headers"].as_array() {
Some(arr) => arr
.iter()
.filter_map(|h| {
let pair = h.as_array()?;
let key: String = pair[0].as_str()?.into();
let raw_val: &str = pair[1].as_str()?;
let val = if encrypted && is_sensitive_key(&key) {
match secrets {
Some(s) => s.decrypt(raw_val),
None => raw_val.to_string(),
}
} else {
raw_val.to_string()
};
Some((key, val))
})
.collect(),
None => vec![],
};
Ok(DataSourceConfig::External {
url,
headers,
api_key,
})
}
}
}
pub fn data_source_from_row(