state recovery, polling optimizations, error rendering

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.
This commit is contained in:
2026-06-19 12:56:12 +02:00
parent 8b1dac9669
commit 13497dd53c
12 changed files with 338 additions and 40 deletions

View File

@@ -5,7 +5,7 @@ use client_domain::{
BoundingBox, Color, DisplayPort, FontMetrics, RenderEngine, ScrollState, ThemeConfig,
};
use client_application::{ClientApp, RepaintCommand};
use domain::{DisplayHint, Value};
use domain::{DisplayHint, Value, WidgetError};
use protocol::ServerMessage;
use super::RenderEvent;
use crate::config::RENDER_POLL_INTERVAL;
@@ -21,6 +21,7 @@ const COLOR_DISCONNECTED: Color = Color(200, 0, 0);
struct WidgetCache {
hint: DisplayHint,
data: Vec<(String, Value)>,
error: Option<WidgetError>,
bounds: BoundingBox,
scroll: ScrollState,
}
@@ -121,13 +122,15 @@ fn update_cache(engine: &RenderEngine, cmd: &RepaintCommand) -> WidgetCache {
.iter()
.map(|kv| (kv.key.clone(), kv.value.clone().into()))
.collect();
let error: Option<WidgetError> = cmd.state.error.as_ref().map(|e| e.clone().into());
let content_h = engine.content_height(&hint, &data, cmd.bounds.width);
let content_h = engine.content_height(&hint, &data, cmd.bounds.width, error.as_ref());
let scroll = ScrollState::new(cmd.bounds.height, content_h);
WidgetCache {
hint,
data,
error,
bounds: cmd.bounds,
scroll,
}
@@ -143,6 +146,7 @@ fn draw_widget(
&cache.data,
cache.bounds,
cache.scroll.offset(),
cache.error.as_ref(),
);
for dc in &draw_cmds {