dashboard: overall sync status; fix unselected spinner visibility
- New SYNC STATUS card on the dashboard: every device with its last sync result (✓ ok / ✗ error / — never) + ages, and an ok/error/never/last summary - Sidebar: the sync spinner is now bright cyan on unselected (dark) rows and dark on the selected (accent) row, so it's visible in both states
This commit is contained in:
@@ -189,10 +189,13 @@ fn draw_device_list(f: &mut Frame, area: Rect, app: &App, syncing: bool) {
|
|||||||
// status icon
|
// status icon
|
||||||
let is_this = sync_device.as_ref().map(|d| d == &device.name).unwrap_or(false);
|
let is_this = sync_device.as_ref().map(|d| d == &device.name).unwrap_or(false);
|
||||||
if is_this {
|
if is_this {
|
||||||
spans.push(Span::styled(
|
// dark on the accent selection, bright cyan on the dark unselected row
|
||||||
app.spinner().to_string(),
|
let spinner_style = if selected {
|
||||||
Style::default().fg(theme::BG),
|
Style::default().fg(theme::BG).add_modifier(Modifier::BOLD)
|
||||||
));
|
} else {
|
||||||
|
Style::default().fg(theme::CYAN).add_modifier(Modifier::BOLD)
|
||||||
|
};
|
||||||
|
spans.push(Span::styled(app.spinner().to_string(), spinner_style));
|
||||||
} else if device.mounted {
|
} else if device.mounted {
|
||||||
spans.push(Span::styled("●", Style::default().fg(if selected { theme::BG } else { theme::GREEN })));
|
spans.push(Span::styled("●", Style::default().fg(if selected { theme::BG } else { theme::GREEN })));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+73
-24
@@ -56,10 +56,10 @@ pub fn draw(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
draw_device_card(f, left, app, device);
|
draw_device_card(f, left, app, device);
|
||||||
draw_settings_card(f, right, device);
|
draw_settings_card(f, right, device);
|
||||||
|
|
||||||
let [lib, pipe] =
|
let [lib, status] =
|
||||||
Layout::horizontal([Constraint::Percentage(52), Constraint::Percentage(48)]).areas(row2);
|
Layout::horizontal([Constraint::Percentage(52), Constraint::Percentage(48)]).areas(row2);
|
||||||
draw_library_card(f, lib, app);
|
draw_library_card(f, lib, app);
|
||||||
draw_pipeline_card(f, pipe, device);
|
draw_sync_status_card(f, status, app);
|
||||||
|
|
||||||
draw_chain(f, chain, app, device);
|
draw_chain(f, chain, app, device);
|
||||||
}
|
}
|
||||||
@@ -247,34 +247,83 @@ fn draw_library_card(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_pipeline_card(f: &mut Frame, area: Rect, device: &crate::config::Device) {
|
fn draw_sync_status_card(f: &mut Frame, area: Rect, app: &App) {
|
||||||
let block = card("SYNC PIPELINE");
|
let block = card("SYNC STATUS");
|
||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
f.render_widget(block, area);
|
f.render_widget(block, area);
|
||||||
|
|
||||||
let steps: Vec<(&str, &str)> = vec![
|
let syncing = app.engine.state.lock().map(|s| s.running).unwrap_or(false);
|
||||||
("1", "mirror StorageBox → local"),
|
let sync_device = if syncing {
|
||||||
("2", "NFC-aware diff"),
|
app.engine.state.lock().unwrap().device.clone()
|
||||||
("3", "rsync copy"),
|
} else {
|
||||||
("4", if device.firmware == Firmware::Mlove {
|
None
|
||||||
"eject + fatsort"
|
};
|
||||||
} else {
|
let now = chrono::Local::now();
|
||||||
"eject"
|
|
||||||
}),
|
let mut ok = 0usize;
|
||||||
];
|
let mut err = 0usize;
|
||||||
|
let mut never = 0usize;
|
||||||
|
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
for (n, label) in steps {
|
for device in &app.config.devices {
|
||||||
lines.push(Line::from(vec![
|
let is_syncing = sync_device.as_ref().map(|d| d == &device.name).unwrap_or(false);
|
||||||
Span::raw(" "),
|
let status_span: Span = if is_syncing {
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(" {n} "),
|
format!("{} syncing…", app.spinner()),
|
||||||
Style::default().fg(theme::BG).bg(theme::ACCENT).add_modifier(Modifier::BOLD),
|
Style::default().fg(theme::CYAN).add_modifier(Modifier::BOLD),
|
||||||
),
|
)
|
||||||
Span::raw(" "),
|
} else if let Some(dt) = device.last_sync {
|
||||||
Span::styled(label, Style::default().fg(theme::FG)),
|
let ago = relative_ago(dt);
|
||||||
]));
|
if device.last_status == "ok" {
|
||||||
lines.push(Line::raw(""));
|
ok += 1;
|
||||||
|
Span::styled(
|
||||||
|
format!("✓ ok · {ago}"),
|
||||||
|
Style::default().fg(theme::GREEN),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
err += 1;
|
||||||
|
Span::styled(
|
||||||
|
format!("✗ error · {ago}"),
|
||||||
|
Style::default().fg(theme::RED).add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
never += 1;
|
||||||
|
Span::styled("— never", Style::default().fg(theme::DIM))
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = truncate(&device.name, inner.width.saturating_sub(26).max(4) as usize);
|
||||||
|
let mut spans: Vec<Span> = vec![Span::raw(" "), Span::styled(name, Style::default().fg(theme::FG))];
|
||||||
|
let used: usize = spans.iter().map(|s| s.width()).sum();
|
||||||
|
let left = inner.width as usize - 2 - status_span.width();
|
||||||
|
if used < left {
|
||||||
|
spans.push(Span::raw(" ".repeat(left - used)));
|
||||||
|
}
|
||||||
|
spans.push(status_span);
|
||||||
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// summary footer
|
||||||
|
lines.push(Line::raw(""));
|
||||||
|
let summary = format!(
|
||||||
|
" {ok} ok · {err} error · {never} never · last {last}",
|
||||||
|
last = app
|
||||||
|
.config
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|d| d.last_sync)
|
||||||
|
.min_by_key(|dt| {
|
||||||
|
let s = now.signed_duration_since(*dt).num_seconds();
|
||||||
|
s.max(0)
|
||||||
|
})
|
||||||
|
.map(|dt| relative_ago(dt))
|
||||||
|
.unwrap_or_else(|| "—".to_string()),
|
||||||
|
);
|
||||||
|
lines.push(Line::from(Span::styled(
|
||||||
|
truncate(&summary, inner.width as usize),
|
||||||
|
Style::default().fg(theme::DIM),
|
||||||
|
)));
|
||||||
|
|
||||||
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
f.render_widget(Paragraph::new(lines).style(Style::default().bg(theme::PANEL_BG)), inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user