per-source polling, initial client state, webhook, preview, client tracking
- per-source poll intervals: spawn task per source with own interval,
manager re-checks sources every 30s for add/remove
- initial screen update on TCP connect: send layout + widget states
- client tracking: ClientRegistry port, GET /api/clients, dashboard list
- webhook adapter: POST /api/webhook/{source_id} feeds data into projection
- widget preview: GET /api/widgets/{id}/preview returns current state
- serve SPA from Axum: ServeDir + index.html fallback via KFRAME_SPA_DIR
- layout builder delete confirmation with AlertDialog
- form validation: required fields disable save button
- guide page at /guide
- fix architecture: ClientDto to api-types, ClientRegistry + WidgetStateReader
ports in domain, DataProjection has internal Mutex, no adapter cross-deps
- ESP32: full screen clear on layout change (stale pixel fix)
This commit is contained in:
@@ -1,15 +1,27 @@
|
||||
use crate::broadcaster::TcpBroadcaster;
|
||||
use crate::client_tracker::ClientTracker;
|
||||
use crate::error::TcpServerError;
|
||||
use domain::{ConfigRepository, WidgetStateReader};
|
||||
use protocol::{ServerMessage, WidgetDescriptor, WireDisplayHint, WireLayoutNode, encode};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{info, warn};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub async fn run_tcp_server(
|
||||
use crate::broadcaster::TcpBroadcaster;
|
||||
|
||||
pub async fn run_tcp_server<C, W>(
|
||||
addr: &str,
|
||||
broadcaster: Arc<TcpBroadcaster>,
|
||||
) -> Result<(), TcpServerError> {
|
||||
tracker: Arc<ClientTracker>,
|
||||
config: Arc<C>,
|
||||
widget_states: Arc<W>,
|
||||
) -> Result<(), TcpServerError>
|
||||
where
|
||||
C: ConfigRepository + Send + Sync + 'static,
|
||||
C::Error: std::fmt::Debug + Send,
|
||||
W: WidgetStateReader + Send + Sync + 'static,
|
||||
{
|
||||
let listener = TcpListener::bind(addr).await.map_err(TcpServerError::Io)?;
|
||||
info!(addr, "TCP server listening");
|
||||
|
||||
@@ -17,9 +29,21 @@ pub async fn run_tcp_server(
|
||||
let (mut socket, peer) = listener.accept().await.map_err(TcpServerError::Io)?;
|
||||
info!(%peer, "client connected");
|
||||
|
||||
tracker.add(peer);
|
||||
let tracker = tracker.clone();
|
||||
let mut rx = broadcaster.subscribe();
|
||||
|
||||
let initial_frame = build_initial_frame(&*config, &*widget_states).await;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(frame) = initial_frame
|
||||
&& socket.write_all(&frame).await.is_err()
|
||||
{
|
||||
info!(%peer, "client disconnected during initial send");
|
||||
tracker.remove(peer);
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(frame) => {
|
||||
@@ -34,6 +58,56 @@ pub async fn run_tcp_server(
|
||||
}
|
||||
}
|
||||
}
|
||||
tracker.remove(peer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_initial_frame<C, W>(config: &C, widget_states: &W) -> Option<Vec<u8>>
|
||||
where
|
||||
C: ConfigRepository,
|
||||
C::Error: std::fmt::Debug,
|
||||
W: WidgetStateReader,
|
||||
{
|
||||
let layout = match config.get_layout().await {
|
||||
Ok(Some(l)) => l,
|
||||
Ok(None) => return None,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "failed to fetch layout for initial send");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let widgets = match config.list_widgets().await {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!(error = ?e, "failed to fetch widgets for initial send");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let wire_layout: WireLayoutNode = (&layout.root).into();
|
||||
let mut wire_widgets = Vec::new();
|
||||
for w in &widgets {
|
||||
if let Some(s) = widget_states.get_widget_state(w.id).await {
|
||||
wire_widgets.push(WidgetDescriptor {
|
||||
id: w.id,
|
||||
display_hint: WireDisplayHint::IconValue,
|
||||
state: (&s).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let msg = ServerMessage::ScreenUpdate {
|
||||
layout: wire_layout,
|
||||
widgets: wire_widgets,
|
||||
};
|
||||
|
||||
match encode(&msg) {
|
||||
Ok(frame) => Some(frame),
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to encode initial screen update");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user