authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-01 22:36:38-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-01 22:42:02-07:00
log426cd8cd8b10a8ebc02af363750ce3144677e062
tree40535f8eaef7864964c35130108ab48e796d5f4b
parent83112c0bfd59fdfe0b0ea81e1fbc64ea9839a008
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: keep sync advice while working offline

Turning Work Offline on during a problem state dropped the advice line, shifting everything below it. The advice now stays, and the toggle test covers every state, password-protected and unreadable sections included. Assisted-by: claude-opus-5.5

1 files changed, 35 insertions(+), 31 deletions(-)

crates/snowbound/src/sync.rs+35-31
...@@ -441,28 +441,25 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {...@@ -441,28 +441,25 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
441 let host = facts.place.first().map_or("the notebook", String::as_str);441 let host = facts.place.first().map_or("the notebook", String::as_str);
442 let waiting = changes(sync.queued);442 let waiting = changes(sync.queued);
443 let conflict;443 let conflict;
444 let advice = if facts.offline {444 // A problem's advice stays while working offline, so turning it on moves nothing.
445 None445 let advice = match state {
446 } else {446 SyncState::NotConnected => Some(match sync.queued {
447 match state {447 0 => format!("Can’t reach {host}. Sync continues when it’s back."),
448 SyncState::NotConnected => Some(match sync.queued {448 _ => format!("Can’t reach {host}. {waiting} will sync when it’s back."),
449 0 => format!("Can’t reach {host}. Sync continues when it’s back."),449 }),
450 _ => format!("Can’t reach {host}. {waiting} will sync when it’s back."),450 SyncState::InUse => {
451 }),451 Some("Someone else is saving a section. Sync continues when they finish.".into())
452 SyncState::InUse => {
453 Some("Someone else is saving a section. Sync continues when they finish.".into())
454 }
455 SyncState::ReadOnly => Some(format!(
456 "This location is read-only. Your changes stay on {THIS}."
457 )),
458 SyncState::Protected => {
459 Some("Snowbound can’t open password-protected sections yet.".into())
460 }
461 SyncState::Unreadable | SyncState::Failed => {
462 Some("Snowbound keeps trying. Sync Now tries again at once.".into())
463 }
464 _ => None,
465 }452 }
453 SyncState::ReadOnly => Some(format!(
454 "This location is read-only. Your changes stay on {THIS}."
455 )),
456 SyncState::Protected => {
457 Some("Snowbound can’t open password-protected sections yet.".into())
458 }
459 SyncState::Unreadable | SyncState::Failed => {
460 Some("Snowbound keeps trying. Sync Now tries again at once.".into())
461 }
462 _ => None,
466 };463 };
467 if !showing && headline == "Up to date" && !facts.conflicts.is_empty() {464 if !showing && headline == "Up to date" && !facts.conflicts.is_empty() {
468 conflict = match facts.conflicts.as_slice() {465 conflict = match facts.conflicts.as_slice() {
...@@ -1380,6 +1377,18 @@ mod tests {...@@ -1380,6 +1377,18 @@ mod tests {
1380 _ => status(true, 0, None),1377 _ => status(true, 0, None),
1381 }))1378 }))
1382 }),1379 }),
1380 ("protected", || {
1381 facts(sections(|index| match index {
1382 1 => status(true, 0, Some((io::ErrorKind::Unsupported, "Protected"))),
1383 _ => status(true, 0, None),
1384 }))
1385 }),
1386 ("unreadable", || {
1387 facts(sections(|index| match index {
1388 4 => status(true, 0, Some((io::ErrorKind::InvalidData, "Not a section"))),
1389 _ => status(true, 0, None),
1390 }))
1391 }),
1383 ("in-use", || {1392 ("in-use", || {
1384 facts(sections(|index| match index {1393 facts(sections(|index| match index {
1385 2 => status(true, 2, Some((io::ErrorKind::ResourceBusy, "In use"))),1394 2 => status(true, 2, Some((io::ErrorKind::ResourceBusy, "In use"))),
...@@ -1433,17 +1442,12 @@ mod tests {...@@ -1433,17 +1442,12 @@ mod tests {
1433 )))1442 )))
1434 }1443 }
14351444
1436 /// Work offline's switch stays put as it turns on and off, and so do the buttons below1445 /// Work offline's switch, and everything below it, stays put as it turns on and off.
1437 /// but where a problem's advice, which offline leaves out, goes and comes back.
1438 #[test]1446 #[test]
1439 fn toggling_offline_moves_nothing() {1447 fn toggling_offline_moves_nothing() {
1440 let switch = id().child("status").child("header").child("offline");1448 let switch = id().child("status").child("header").child("offline");
1441 let controls = id().child("controls");1449 let controls = id().child("controls");
1442 for (name, made) in states() {1450 for (name, made) in states() {
1443 let advised = !matches!(
1444 overall(&made().sections).state(),
1445 SyncState::UpToDate | SyncState::Syncing
1446 );
1447 let mut places = Vec::new();1451 let mut places = Vec::new();
1448 for offline in [false, true, false] {1452 for offline in [false, true, false] {
1449 let mut ui = ui(Appearance::Light);1453 let mut ui = ui(Appearance::Light);
...@@ -1452,10 +1456,10 @@ mod tests {...@@ -1452,10 +1456,10 @@ mod tests {
1452 settle(&mut ui, &mut now, &facts);1456 settle(&mut ui, &mut now, &facts);
1453 places.push((ui.rect(switch).unwrap(), ui.rect(controls).unwrap()));1457 places.push((ui.rect(switch).unwrap(), ui.rect(controls).unwrap()));
1454 }1458 }
1455 for pair in places.windows(2) {1459 assert!(
1456 assert_eq!(pair[0].0, pair[1].0, "{name}");1460 places.windows(2).all(|pair| pair[0] == pair[1]),
1457 assert!(advised || pair[0].1 == pair[1].1, "{name}: {places:?}");1461 "{name}: {places:?}"
1458 }1462 );
1459 }1463 }
1460 }1464 }
14611465