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

@@ -2,7 +2,7 @@ use crate::{
BoundingBox, Color, FontMetrics, FontSize, ThemeConfig, alignment::align_offset,
markup::parse_markup, text_layout::wrap_lines,
};
use domain::{DisplayHint, DisplayHintKind, HAlign, VAlign, Value};
use domain::{DisplayHint, DisplayHintKind, HAlign, VAlign, Value, WidgetError};
#[derive(Debug, Clone, PartialEq)]
pub struct DrawCommand {
@@ -92,8 +92,12 @@ impl RenderEngine {
data: &[(String, Value)],
bounds: BoundingBox,
scroll_offset: u16,
error: Option<&WidgetError>,
) -> Vec<DrawCommand> {
let text = self.format_widget(hint, data);
let mut text = self.format_widget(hint, data);
if error.is_some() {
text = format!("{{accent}}[offline]{{/}}\n{text}");
}
let mut cmds = self.render_text(&text, bounds, hint.h_align, hint.v_align);
if scroll_offset > 0 {
@@ -110,8 +114,17 @@ impl RenderEngine {
cmds
}
pub fn content_height(&self, hint: &DisplayHint, data: &[(String, Value)], width: u16) -> u16 {
let text = self.format_widget(hint, data);
pub fn content_height(
&self,
hint: &DisplayHint,
data: &[(String, Value)],
width: u16,
error: Option<&WidgetError>,
) -> u16 {
let mut text = self.format_widget(hint, data);
if error.is_some() {
text = format!("{{accent}}[offline]{{/}}\n{text}");
}
let plain: String = parse_markup(&text, &self.theme)
.iter()
.map(|s| s.text.as_str())