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

@@ -14,26 +14,32 @@ pub enum HttpJsonError {
Parse(String),
}
impl HttpJsonAdapter {
pub fn new() -> Self {
impl Default for HttpJsonAdapter {
fn default() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
impl HttpJsonAdapter {
pub fn new() -> Self {
Self::default()
}
}
fn json_to_value(json: serde_json::Value) -> Value {
match json {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(0.0)),
serde_json::Value::String(s) => Value::String(s),
serde_json::Value::Array(arr) => {
Value::Array(arr.into_iter().map(json_to_value).collect())
}
serde_json::Value::Object(map) => {
Value::Object(map.into_iter().map(|(k, v)| (k, json_to_value(v))).collect())
}
serde_json::Value::Array(arr) => Value::Array(arr.into_iter().map(json_to_value).collect()),
serde_json::Value::Object(map) => Value::Object(
map.into_iter()
.map(|(k, v)| (k, json_to_value(v)))
.collect(),
),
}
}

View File

@@ -1,19 +1,23 @@
use std::time::Duration;
use axum::{Router, routing::get, response::Json};
use axum::{Router, response::Json, routing::get};
use domain::{DataSource, DataSourceConfig, DataSourcePort, DataSourceType, Value};
use http_json::HttpJsonAdapter;
use std::time::Duration;
async fn start_fake_api() -> String {
let app = Router::new()
.route("/weather", get(|| async {
Json(serde_json::json!({
"main": {"temp": 5.4, "humidity": 80},
"weather": [{"icon": "cloud_rain"}]
}))
}))
.route("/simple", get(|| async {
Json(serde_json::json!({"value": "hello"}))
}))
.route(
"/weather",
get(|| async {
Json(serde_json::json!({
"main": {"temp": 5.4, "humidity": 80},
"weather": [{"icon": "cloud_rain"}]
}))
}),
)
.route(
"/simple",
get(|| async { Json(serde_json::json!({"value": "hello"})) }),
)
.route("/not-json", get(|| async { "plain text" }));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
@@ -46,10 +50,7 @@ async fn polls_url_and_returns_nested_json_as_value() {
let result = adapter.poll(&source).await.unwrap();
assert_eq!(
result.get_path("$.main.temp"),
Some(&Value::Number(5.4))
);
assert_eq!(result.get_path("$.main.temp"), Some(&Value::Number(5.4)));
assert_eq!(
result.get_path("$.main.humidity"),
Some(&Value::Number(80.0))