Compare commits
2 Commits
437056cfc4
...
a6152c9a9a
| Author | SHA1 | Date | |
|---|---|---|---|
| a6152c9a9a | |||
| 455d5da901 |
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -226,9 +226,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"application",
|
"application",
|
||||||
"chrono",
|
|
||||||
"chrono-tz",
|
|
||||||
"config-sqlite",
|
"config-sqlite",
|
||||||
|
"data-generators",
|
||||||
"domain",
|
"domain",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"http-api",
|
"http-api",
|
||||||
@@ -467,6 +466,16 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-generators"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"domain",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ members = [
|
|||||||
"crates/adapters/media",
|
"crates/adapters/media",
|
||||||
"crates/adapters/auth",
|
"crates/adapters/auth",
|
||||||
"crates/adapters/secret-store",
|
"crates/adapters/secret-store",
|
||||||
|
"crates/adapters/data-generators",
|
||||||
"crates/api-types",
|
"crates/api-types",
|
||||||
"crates/bootstrap",
|
"crates/bootstrap",
|
||||||
"crates/client-desktop",
|
"crates/client-desktop",
|
||||||
@@ -57,5 +58,4 @@ postcard = { version = "1.1", default-features = false, features = ["alloc"] }
|
|||||||
tokio = { version = "1.0", features = ["macros", "rt", "rt-multi-thread", "net", "sync", "time", "io-util"] }
|
tokio = { version = "1.0", features = ["macros", "rt", "rt-multi-thread", "net", "sync", "time", "io-util"] }
|
||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
chrono = "0.4"
|
data-generators = { path = "crates/adapters/data-generators" }
|
||||||
chrono-tz = "0.10"
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Hexagonal / ports-and-adapters with full CQRS. Domain logic has zero framework d
|
|||||||
│ tcp-server binary protocol broadcast │
|
│ tcp-server binary protocol broadcast │
|
||||||
│ http-json external API polling │
|
│ http-json external API polling │
|
||||||
│ media, rss source-specific adapters │
|
│ media, rss source-specific adapters │
|
||||||
|
│ data-generators clock, static text │
|
||||||
│ auth argon2 + JWT │
|
│ auth argon2 + JWT │
|
||||||
├─────────────────── Shared ───────────────────┤
|
├─────────────────── Shared ───────────────────┤
|
||||||
│ protocol/ wire types, postcard serde │
|
│ protocol/ wire types, postcard serde │
|
||||||
@@ -53,10 +54,12 @@ See `docs/adr/` for architectural decision records and `CONTEXT.md` for the doma
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Data sources**: HTTP/JSON, weather, media (Subsonic/Navidrome), RSS, webhooks
|
- **Data sources**: HTTP/JSON, weather, media (Subsonic/Navidrome), RSS, webhooks, clock, static text
|
||||||
- **Layout engine**: flexbox-like containers (row/column, fixed/flex sizing, gap, padding, justify-content, align-items)
|
- **Layout engine**: flexbox-like containers (row/column, fixed/flex sizing, gap, padding, justify-content, align-items)
|
||||||
- **Theming**: 5 configurable colors (primary, secondary, accent, text, background), live push to clients
|
- **Theming**: 5 configurable colors (primary, secondary, accent, text, background), live push to clients
|
||||||
- **Rich text**: inline color markup (`{primary}text{/}`, `{#FF0000}hex{/}`)
|
- **Rich text**: inline color markup (`{primary}text{/}`, `{#FF0000}hex{/}`)
|
||||||
|
- **Widget alignment**: per-widget horizontal/vertical text alignment (left/center/right, top/middle/bottom), reflected in layout preview
|
||||||
|
- **Connection indicator**: green/red dot on ESP32 display showing server connectivity
|
||||||
- **Overflow scroll**: bounce animation when content exceeds widget bounds, speed auto-derived from overflow
|
- **Overflow scroll**: bounce animation when content exceeds widget bounds, speed auto-derived from overflow
|
||||||
- **Captive portal**: ESP32 AP mode with DNS + HTTP config form for WiFi provisioning
|
- **Captive portal**: ESP32 AP mode with DNS + HTTP config form for WiFi provisioning
|
||||||
- **Auth**: argon2 password hashing, JWT tokens, protected API routes
|
- **Auth**: argon2 password hashing, JWT tokens, protected API routes
|
||||||
|
|||||||
10
crates/adapters/data-generators/Cargo.toml
Normal file
10
crates/adapters/data-generators/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "data-generators"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
domain.workspace = true
|
||||||
|
chrono = "0.4"
|
||||||
|
chrono-tz = "0.10"
|
||||||
|
thiserror.workspace = true
|
||||||
59
crates/adapters/data-generators/src/lib.rs
Normal file
59
crates/adapters/data-generators/src/lib.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use domain::{DataSource, DataSourceConfig, DataSourcePort, Value};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ClockGenerator;
|
||||||
|
|
||||||
|
impl ClockGenerator {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum GeneratorError {
|
||||||
|
#[error("wrong config type for generator")]
|
||||||
|
WrongConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataSourcePort for ClockGenerator {
|
||||||
|
type Error = GeneratorError;
|
||||||
|
|
||||||
|
async fn poll(&self, source: &DataSource) -> Result<Value, Self::Error> {
|
||||||
|
let (fmt, tz_name) = match &source.config {
|
||||||
|
DataSourceConfig::Clock { format, timezone } => (format.as_str(), timezone.as_str()),
|
||||||
|
_ => ("%H:%M:%S", "UTC"),
|
||||||
|
};
|
||||||
|
let tz: Tz = tz_name.parse().unwrap_or(chrono_tz::UTC);
|
||||||
|
let now = Utc::now().with_timezone(&tz);
|
||||||
|
let formatted = now.format(fmt).to_string();
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert("time".into(), Value::String(formatted));
|
||||||
|
Ok(Value::Object(map))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct StaticTextGenerator;
|
||||||
|
|
||||||
|
impl StaticTextGenerator {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataSourcePort for StaticTextGenerator {
|
||||||
|
type Error = GeneratorError;
|
||||||
|
|
||||||
|
async fn poll(&self, source: &DataSource) -> Result<Value, Self::Error> {
|
||||||
|
let text = match &source.config {
|
||||||
|
DataSourceConfig::StaticText { text } => text.clone(),
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert("text".into(), Value::String(text));
|
||||||
|
Ok(Value::Object(map))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use crate::AppState;
|
|||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::Json;
|
use axum::response::Json;
|
||||||
use domain::{BroadcastPort, ConfigRepository, EventPublisher, WidgetStateReader};
|
use domain::{ConfigRepository, DomainEvent, EventPublisher};
|
||||||
|
|
||||||
type S<C, E, W, B, R, A, H> = State<AppState<C, E, W, B, R, A, H>>;
|
type S<C, E, W, B, R, A, H> = State<AppState<C, E, W, B, R, A, H>>;
|
||||||
|
|
||||||
@@ -16,9 +16,6 @@ where
|
|||||||
C::Error: std::fmt::Debug,
|
C::Error: std::fmt::Debug,
|
||||||
E: EventPublisher,
|
E: EventPublisher,
|
||||||
E::Error: std::fmt::Debug,
|
E::Error: std::fmt::Debug,
|
||||||
W: WidgetStateReader,
|
|
||||||
B: BroadcastPort,
|
|
||||||
B::Error: std::fmt::Debug,
|
|
||||||
{
|
{
|
||||||
let source = state
|
let source = state
|
||||||
.config
|
.config
|
||||||
@@ -34,37 +31,14 @@ where
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let raw = json_to_domain_value(body);
|
let data = json_to_domain_value(body);
|
||||||
let widgets = state
|
|
||||||
.config
|
state
|
||||||
.list_widgets()
|
.events
|
||||||
|
.publish(DomainEvent::WebhookDataReceived { source_id, data })
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
||||||
|
|
||||||
let layout = state
|
|
||||||
.config
|
|
||||||
.get_layout()
|
|
||||||
.await
|
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
|
||||||
|
|
||||||
let changed = state
|
|
||||||
.widget_states
|
|
||||||
.apply_raw_data(source_id, &raw, &widgets)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !changed.is_empty()
|
|
||||||
&& let Some(l) = &layout
|
|
||||||
{
|
|
||||||
let with_hints: Vec<_> = changed
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(id, s)| {
|
|
||||||
let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone();
|
|
||||||
Some((*id, hint, s.clone()))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let _ = state.broadcaster.push_screen_update(l, &with_hints).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,5 +19,4 @@ anyhow.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
dotenvy.workspace = true
|
dotenvy.workspace = true
|
||||||
chrono.workspace = true
|
data-generators.workspace = true
|
||||||
chrono-tz.workspace = true
|
|
||||||
|
|||||||
@@ -40,6 +40,37 @@ pub async fn run(
|
|||||||
|
|
||||||
info!("layout changed, pushed screen update to clients");
|
info!("layout changed, pushed screen update to clients");
|
||||||
}
|
}
|
||||||
|
Ok(DomainEvent::WebhookDataReceived { source_id, data }) => {
|
||||||
|
let widgets = match config.list_widgets().await {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "failed to fetch widgets for webhook");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let changed = projection
|
||||||
|
.apply_poll_result(source_id, &data, &widgets)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !changed.is_empty() {
|
||||||
|
let with_hints: Vec<_> = changed
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, state)| {
|
||||||
|
let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone();
|
||||||
|
Some((*id, hint, state.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Err(e) = broadcaster.push_data_update(&with_hints).await {
|
||||||
|
error!(error = %e, "failed to push webhook data update");
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
source_id,
|
||||||
|
count = changed.len(),
|
||||||
|
"webhook data received, pushed update"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(DomainEvent::ThemeChanged { theme }) => {
|
Ok(DomainEvent::ThemeChanged { theme }) => {
|
||||||
if let Err(e) = broadcaster.push_theme_update(&theme).await {
|
if let Err(e) = broadcaster.push_theme_update(&theme).await {
|
||||||
error!(error = %e, "failed to push theme update");
|
error!(error = %e, "failed to push theme update");
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use application::DataProjection;
|
use application::DataProjection;
|
||||||
use chrono::Utc;
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use config_sqlite::SqliteConfigStore;
|
use config_sqlite::SqliteConfigStore;
|
||||||
|
use data_generators::{ClockGenerator, StaticTextGenerator};
|
||||||
use domain::{
|
use domain::{
|
||||||
BroadcastPort, ConfigRepository, DataSource, DataSourceConfig, DataSourcePort, DataSourceType,
|
BroadcastPort, ConfigRepository, DataSource, DataSourcePort, DataSourceType, Value, WidgetState,
|
||||||
Value, WidgetState,
|
|
||||||
};
|
};
|
||||||
use http_json::HttpJsonAdapter;
|
use http_json::HttpJsonAdapter;
|
||||||
use media_adapter::MediaAdapter;
|
use media_adapter::MediaAdapter;
|
||||||
use rss_adapter::RssAdapter;
|
use rss_adapter::RssAdapter;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tcp_server::TcpBroadcaster;
|
use tcp_server::TcpBroadcaster;
|
||||||
@@ -19,15 +17,63 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
const SOURCE_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
|
const SOURCE_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Adapters {
|
||||||
|
http: Arc<HttpJsonAdapter>,
|
||||||
|
media: Arc<MediaAdapter>,
|
||||||
|
rss: Arc<RssAdapter>,
|
||||||
|
clock: Arc<ClockGenerator>,
|
||||||
|
static_text: Arc<StaticTextGenerator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Adapters {
|
||||||
|
async fn poll(&self, source: &DataSource) -> Result<Value> {
|
||||||
|
match source.source_type {
|
||||||
|
DataSourceType::HttpJson | DataSourceType::Weather => self
|
||||||
|
.http
|
||||||
|
.poll(source)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}")),
|
||||||
|
DataSourceType::Media => self
|
||||||
|
.media
|
||||||
|
.poll(source)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}")),
|
||||||
|
DataSourceType::Rss => self
|
||||||
|
.rss
|
||||||
|
.poll(source)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}")),
|
||||||
|
DataSourceType::Clock => self
|
||||||
|
.clock
|
||||||
|
.poll(source)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}")),
|
||||||
|
DataSourceType::StaticText => self
|
||||||
|
.static_text
|
||||||
|
.poll(source)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}")),
|
||||||
|
DataSourceType::Webhook => Err(anyhow::anyhow!(
|
||||||
|
"webhook sources are push-based, not polled"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
config: Arc<SqliteConfigStore>,
|
config: Arc<SqliteConfigStore>,
|
||||||
broadcaster: Arc<TcpBroadcaster>,
|
broadcaster: Arc<TcpBroadcaster>,
|
||||||
projection: Arc<DataProjection>,
|
projection: Arc<DataProjection>,
|
||||||
_poll_interval_secs: u64,
|
_poll_interval_secs: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let http_adapter = Arc::new(HttpJsonAdapter::new());
|
let adapters = Adapters {
|
||||||
let media_adapter = Arc::new(MediaAdapter::new());
|
http: Arc::new(HttpJsonAdapter::new()),
|
||||||
let rss_adapter = Arc::new(RssAdapter::new());
|
media: Arc::new(MediaAdapter::new()),
|
||||||
|
rss: Arc::new(RssAdapter::new()),
|
||||||
|
clock: Arc::new(ClockGenerator::new()),
|
||||||
|
static_text: Arc::new(StaticTextGenerator::new()),
|
||||||
|
};
|
||||||
|
|
||||||
let mut running: HashMap<u16, JoinHandle<()>> = HashMap::new();
|
let mut running: HashMap<u16, JoinHandle<()>> = HashMap::new();
|
||||||
|
|
||||||
@@ -64,9 +110,7 @@ pub async fn run(
|
|||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
let broadcaster = broadcaster.clone();
|
let broadcaster = broadcaster.clone();
|
||||||
let projection = projection.clone();
|
let projection = projection.clone();
|
||||||
let http = http_adapter.clone();
|
let adapters = adapters.clone();
|
||||||
let media = media_adapter.clone();
|
|
||||||
let rss = rss_adapter.clone();
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
source_id = source.id,
|
source_id = source.id,
|
||||||
@@ -76,7 +120,7 @@ pub async fn run(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
poll_loop(source, config, broadcaster, projection, http, media, rss).await;
|
poll_loop(source, config, broadcaster, projection, adapters).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
running.insert(source_id, handle);
|
running.insert(source_id, handle);
|
||||||
@@ -95,16 +139,14 @@ async fn poll_loop(
|
|||||||
config: Arc<SqliteConfigStore>,
|
config: Arc<SqliteConfigStore>,
|
||||||
broadcaster: Arc<TcpBroadcaster>,
|
broadcaster: Arc<TcpBroadcaster>,
|
||||||
projection: Arc<DataProjection>,
|
projection: Arc<DataProjection>,
|
||||||
http_adapter: Arc<HttpJsonAdapter>,
|
adapters: Adapters,
|
||||||
media_adapter: Arc<MediaAdapter>,
|
|
||||||
rss_adapter: Arc<RssAdapter>,
|
|
||||||
) {
|
) {
|
||||||
let interval = source.poll_interval;
|
let interval = source.poll_interval;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(interval).await;
|
tokio::time::sleep(interval).await;
|
||||||
|
|
||||||
let result = match poll_source(&http_adapter, &media_adapter, &rss_adapter, &source).await {
|
let result = match adapters.poll(&source).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(source = %source.name, error = %e, "poll failed");
|
warn!(source = %source.name, error = %e, "poll failed");
|
||||||
@@ -139,53 +181,3 @@ async fn poll_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poll_source(
|
|
||||||
http_adapter: &HttpJsonAdapter,
|
|
||||||
media_adapter: &MediaAdapter,
|
|
||||||
rss_adapter: &RssAdapter,
|
|
||||||
source: &DataSource,
|
|
||||||
) -> Result<Value> {
|
|
||||||
match source.source_type {
|
|
||||||
DataSourceType::HttpJson | DataSourceType::Weather => http_adapter
|
|
||||||
.poll(source)
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!("{e}")),
|
|
||||||
DataSourceType::Media => media_adapter
|
|
||||||
.poll(source)
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!("{e}")),
|
|
||||||
DataSourceType::Rss => rss_adapter
|
|
||||||
.poll(source)
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!("{e}")),
|
|
||||||
DataSourceType::Clock => Ok(generate_clock(&source.config)),
|
|
||||||
DataSourceType::StaticText => Ok(generate_static_text(&source.config)),
|
|
||||||
DataSourceType::Webhook => Err(anyhow::anyhow!(
|
|
||||||
"webhook sources are push-based, not polled"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_clock(config: &DataSourceConfig) -> Value {
|
|
||||||
let (fmt, tz_name) = match config {
|
|
||||||
DataSourceConfig::Clock { format, timezone } => (format.as_str(), timezone.as_str()),
|
|
||||||
_ => ("%H:%M:%S", "UTC"),
|
|
||||||
};
|
|
||||||
let tz: Tz = tz_name.parse().unwrap_or(chrono_tz::UTC);
|
|
||||||
let now = Utc::now().with_timezone(&tz);
|
|
||||||
let formatted = now.format(fmt).to_string();
|
|
||||||
let mut map = BTreeMap::new();
|
|
||||||
map.insert("time".into(), Value::String(formatted));
|
|
||||||
Value::Object(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_static_text(config: &DataSourceConfig) -> Value {
|
|
||||||
let text = match config {
|
|
||||||
DataSourceConfig::StaticText { text } => text.clone(),
|
|
||||||
_ => String::new(),
|
|
||||||
};
|
|
||||||
let mut map = BTreeMap::new();
|
|
||||||
map.insert("text".into(), Value::String(text));
|
|
||||||
Value::Object(map)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,43 @@
|
|||||||
use crate::entities::{DataSourceId, LayoutPresetId, WidgetId};
|
use crate::entities::{DataSourceId, LayoutPresetId, WidgetId};
|
||||||
use crate::value_objects::{Layout, ThemeConfig};
|
use crate::value_objects::{Layout, ThemeConfig, Value};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DomainEvent {
|
pub enum DomainEvent {
|
||||||
WidgetCreated { id: WidgetId },
|
WidgetCreated {
|
||||||
WidgetUpdated { id: WidgetId },
|
id: WidgetId,
|
||||||
WidgetDeleted { id: WidgetId },
|
},
|
||||||
DataSourceAdded { id: DataSourceId },
|
WidgetUpdated {
|
||||||
DataSourceUpdated { id: DataSourceId },
|
id: WidgetId,
|
||||||
DataSourceRemoved { id: DataSourceId },
|
},
|
||||||
LayoutChanged { layout: Layout },
|
WidgetDeleted {
|
||||||
ThemeChanged { theme: ThemeConfig },
|
id: WidgetId,
|
||||||
LayoutPresetSaved { id: LayoutPresetId },
|
},
|
||||||
LayoutPresetLoaded { id: LayoutPresetId },
|
DataSourceAdded {
|
||||||
LayoutPresetDeleted { id: LayoutPresetId },
|
id: DataSourceId,
|
||||||
|
},
|
||||||
|
DataSourceUpdated {
|
||||||
|
id: DataSourceId,
|
||||||
|
},
|
||||||
|
DataSourceRemoved {
|
||||||
|
id: DataSourceId,
|
||||||
|
},
|
||||||
|
LayoutChanged {
|
||||||
|
layout: Layout,
|
||||||
|
},
|
||||||
|
ThemeChanged {
|
||||||
|
theme: ThemeConfig,
|
||||||
|
},
|
||||||
|
LayoutPresetSaved {
|
||||||
|
id: LayoutPresetId,
|
||||||
|
},
|
||||||
|
LayoutPresetLoaded {
|
||||||
|
id: LayoutPresetId,
|
||||||
|
},
|
||||||
|
LayoutPresetDeleted {
|
||||||
|
id: LayoutPresetId,
|
||||||
|
},
|
||||||
|
WebhookDataReceived {
|
||||||
|
source_id: DataSourceId,
|
||||||
|
data: Value,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user