add SPA config UI, wire media/rss adapters, event-driven layout push

- React SPA: dashboard, data sources CRUD, widgets CRUD, layout builder,
  presets. TanStack Router + Query, shadcn/ui, Vite proxy to :3000
- wire media + rss adapters into polling loop, remove xtb source type
- media adapter: read username/password from headers, proper subsonic auth
- event handler: subscribe to LayoutChanged, push screen update to clients
- fix clippy warnings across workspace (Default impls, collapsible ifs,
  redundant closures, is_none_or, unused imports)
This commit is contained in:
2026-06-19 00:12:42 +02:00
parent 21c08911df
commit 26ebfad3a2
175 changed files with 12338 additions and 801 deletions

View File

@@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
use std::time::Duration;
use domain::*;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Serialize, Deserialize)]
pub struct DataSourceDto {
@@ -21,11 +21,12 @@ impl From<&DataSource> for DataSourceDto {
source_type: match ds.source_type {
DataSourceType::Weather => "weather",
DataSourceType::Media => "media",
DataSourceType::Xtb => "xtb",
DataSourceType::Rss => "rss",
DataSourceType::HttpJson => "http_json",
DataSourceType::Webhook => "webhook",
}.into(),
}
.into(),
poll_interval_secs: ds.poll_interval.as_secs(),
url: ds.config.url.clone(),
api_key: ds.config.api_key.clone(),
@@ -39,7 +40,7 @@ impl DataSourceDto {
let source_type = match self.source_type.as_str() {
"weather" => DataSourceType::Weather,
"media" => DataSourceType::Media,
"xtb" => DataSourceType::Xtb,
"rss" => DataSourceType::Rss,
"http_json" => DataSourceType::HttpJson,
"webhook" => DataSourceType::Webhook,

View File

@@ -1,5 +1,5 @@
use serde::{Serialize, Deserialize};
use domain::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct SizingDto {
@@ -41,30 +41,41 @@ impl From<&LayoutNode> for LayoutNodeDto {
LayoutNode::Leaf(id) => Self {
node_type: "leaf".into(),
widget_id: Some(*id),
direction: None, gap: None, padding: None, children: None,
direction: None,
gap: None,
padding: None,
children: None,
},
LayoutNode::Container(c) => Self {
node_type: "container".into(),
widget_id: None,
direction: Some(match c.direction {
Direction::Row => "row",
Direction::Column => "column",
}.into()),
direction: Some(
match c.direction {
Direction::Row => "row",
Direction::Column => "column",
}
.into(),
),
gap: Some(c.gap),
padding: Some(c.padding),
children: Some(c.children.iter().map(|ch| LayoutChildDto {
sizing: SizingDto {
sizing_type: match ch.sizing {
Sizing::Fixed(_) => "fixed".into(),
Sizing::Flex(_) => "flex".into(),
},
value: match ch.sizing {
Sizing::Fixed(v) => v,
Sizing::Flex(v) => v as u16,
},
},
node: (&ch.node).into(),
}).collect()),
children: Some(
c.children
.iter()
.map(|ch| LayoutChildDto {
sizing: SizingDto {
sizing_type: match ch.sizing {
Sizing::Fixed(_) => "fixed".into(),
Sizing::Flex(_) => "flex".into(),
},
value: match ch.sizing {
Sizing::Fixed(v) => v,
Sizing::Flex(v) => v as u16,
},
},
node: (&ch.node).into(),
})
.collect(),
),
},
}
}
@@ -83,7 +94,9 @@ impl LayoutNodeDto {
"column" => Direction::Column,
d => return Err(format!("unknown direction: {d}")),
};
let children = self.children.ok_or("missing children")?
let children = self
.children
.ok_or("missing children")?
.into_iter()
.map(|ch| {
let sizing = match ch.sizing.sizing_type.as_str() {
@@ -110,12 +123,16 @@ impl LayoutNodeDto {
impl From<&Layout> for LayoutDto {
fn from(l: &Layout) -> Self {
Self { root: (&l.root).into() }
Self {
root: (&l.root).into(),
}
}
}
impl LayoutDto {
pub fn into_domain(self) -> Result<Layout, String> {
Ok(Layout { root: self.root.into_domain()? })
Ok(Layout {
root: self.root.into_domain()?,
})
}
}

View File

@@ -1,9 +1,9 @@
pub mod widget;
pub mod data_source;
pub mod layout;
pub mod preset;
pub mod widget;
pub use widget::{KeyMappingDto, WidgetDto, CreateWidgetDto};
pub use data_source::DataSourceDto;
pub use layout::{LayoutDto, LayoutNodeDto, LayoutChildDto, SizingDto};
pub use preset::{PresetDto, CreatePresetDto};
pub use layout::{LayoutChildDto, LayoutDto, LayoutNodeDto, SizingDto};
pub use preset::{CreatePresetDto, PresetDto};
pub use widget::{CreateWidgetDto, KeyMappingDto, WidgetDto};

View File

@@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
use domain::*;
use crate::layout::LayoutDto;
use domain::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct PresetDto {

View File

@@ -1,5 +1,5 @@
use serde::{Serialize, Deserialize};
use domain::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct KeyMappingDto {
@@ -28,7 +28,9 @@ pub struct CreateWidgetDto {
pub max_data_size: u16,
}
fn default_max_data_size() -> u16 { 2048 }
fn default_max_data_size() -> u16 {
2048
}
impl From<&WidgetConfig> for WidgetDto {
fn from(w: &WidgetConfig) -> Self {
@@ -39,12 +41,17 @@ impl From<&WidgetConfig> for WidgetDto {
DisplayHint::IconValue => "icon_value",
DisplayHint::TextBlock => "text_block",
DisplayHint::KeyValue => "key_value",
}.into(),
}
.into(),
data_source_id: w.data_source_id,
mappings: w.mappings.iter().map(|m| KeyMappingDto {
source_path: m.source_path.clone(),
target_key: m.target_key.clone(),
}).collect(),
mappings: w
.mappings
.iter()
.map(|m| KeyMappingDto {
source_path: m.source_path.clone(),
target_key: m.target_key.clone(),
})
.collect(),
max_data_size: w.max_data_size,
}
}
@@ -63,10 +70,14 @@ impl CreateWidgetDto {
name: self.name,
display_hint: hint,
data_source_id: self.data_source_id,
mappings: self.mappings.into_iter().map(|m| KeyMapping {
source_path: m.source_path,
target_key: m.target_key,
}).collect(),
mappings: self
.mappings
.into_iter()
.map(|m| KeyMapping {
source_path: m.source_path,
target_key: m.target_key,
})
.collect(),
max_data_size: self.max_data_size,
})
}