widget states cached to SQLite, loaded on startup to seed DataProjection so server restart preserves last-known data for reconnecting clients. polling: first poll runs immediately, widget list cached per-task with 30s refresh, static text polled once inline instead of looping. poll failures propagate WidgetError::SourceUnavailable to clients. render engine prepends [offline] prefix in accent color, stale data preserved below.
119 lines
3.2 KiB
Rust
119 lines
3.2 KiB
Rust
pub mod error;
|
|
mod repository;
|
|
mod serialization;
|
|
|
|
use domain::SecretStore;
|
|
use sqlx::SqlitePool;
|
|
use std::sync::Arc;
|
|
|
|
pub use error::SqliteConfigError;
|
|
|
|
pub struct SqliteConfigStore {
|
|
pool: SqlitePool,
|
|
secrets: Option<Arc<dyn SecretStore + Send + Sync>>,
|
|
}
|
|
|
|
impl SqliteConfigStore {
|
|
pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
|
|
Self::with_secrets(database_url, None).await
|
|
}
|
|
|
|
pub async fn with_secrets(
|
|
database_url: &str,
|
|
secrets: Option<Arc<dyn SecretStore + Send + Sync>>,
|
|
) -> Result<Self, sqlx::Error> {
|
|
let pool = SqlitePool::connect(database_url).await?;
|
|
let store = Self { pool, secrets };
|
|
store.migrate().await?;
|
|
Ok(store)
|
|
}
|
|
|
|
pub(crate) fn secrets(&self) -> Option<&(dyn SecretStore + Send + Sync)> {
|
|
self.secrets.as_deref()
|
|
}
|
|
|
|
async fn migrate(&self) -> Result<(), sqlx::Error> {
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS widgets (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
display_hint TEXT NOT NULL,
|
|
data_source_id INTEGER NOT NULL,
|
|
mappings TEXT NOT NULL,
|
|
max_data_size INTEGER NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS data_sources (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
source_type TEXT NOT NULL,
|
|
poll_interval_secs INTEGER NOT NULL,
|
|
config TEXT NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS layout (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
data TEXT NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS presets (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
layout_data TEXT NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS theme (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
data TEXT NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
sqlx::query(
|
|
"CREATE TABLE IF NOT EXISTS widget_state_cache (
|
|
widget_id INTEGER PRIMARY KEY,
|
|
state_json TEXT NOT NULL
|
|
)",
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
|
|
// Add alignment columns to widgets (idempotent)
|
|
let _ = sqlx::query("ALTER TABLE widgets ADD COLUMN h_align TEXT NOT NULL DEFAULT 'left'")
|
|
.execute(&self.pool)
|
|
.await;
|
|
let _ = sqlx::query("ALTER TABLE widgets ADD COLUMN v_align TEXT NOT NULL DEFAULT 'top'")
|
|
.execute(&self.pool)
|
|
.await;
|
|
|
|
Ok(())
|
|
}
|
|
}
|