From ec5f14166a5b62d8888c056c19c7afcf67ccabf3 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 19 Apr 2026 08:32:11 -0400 Subject: [PATCH] feat(federation): periodic sync every 30 minutes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now federation.sync-state only fired on (a) user clicking Sync in the UI or (b) server-name push. That meant own_fips_npub, last_transport, peer state updates — all the things v1.5 added for auto-upgrade from Tor to FIPS — didn't propagate until the user poked the button. Fix: spawn a background task in server.rs that runs federation::sync_with_peer for every Trusted peer every 30 minutes. First run is 60s after boot (let onboarding settle) and peers are staggered 5s apart to not hammer Tor's SOCKS proxy with concurrent connects. The sync path already prefers FIPS (via PeerRequest), so once peers have learned each other's fips_npub (now automatic thanks to the own_fips_npub broadcast in state snapshots), subsequent periodic syncs route over FIPS — transport badge cycles from 'tor' to 'fips' on its own without user action. Covers task #30. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/archipelago/src/server.rs | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 742d7c6b..651901e1 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -385,6 +385,69 @@ impl Server { }); } + // Periodic federation state sync — every 30 min we call + // federation::sync_with_peer on each Trusted peer. Without this + // users had to manually click Sync for `fips_npub`/transport + // badge/state updates to propagate; now it happens in the + // background. Staggers peers with a 5s delay so we don't thunder + // the Tor SOCKS proxy. Sync itself already prefers FIPS. + { + let data_dir = config.data_dir.clone(); + let state = state_manager.clone(); + tokio::spawn(async move { + // First run 60s after boot to let onboarding settle. + tokio::time::sleep(Duration::from_secs(60)).await; + let mut interval = tokio::time::interval(Duration::from_secs(1800)); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + loop { + interval.tick().await; + let Ok(nodes) = crate::federation::load_nodes(&data_dir).await else { + continue; + }; + if nodes.is_empty() { + continue; + } + let (data, _) = state.get_snapshot().await; + let Ok(local_did) = + crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey) + else { + continue; + }; + let identity_dir = data_dir.join("identity"); + let Ok(node_identity) = + crate::identity::NodeIdentity::load_or_create(&identity_dir).await + else { + continue; + }; + + for node in &nodes { + if node.trust_level == crate::federation::TrustLevel::Untrusted { + continue; + } + match crate::federation::sync_with_peer( + &data_dir, + node, + &local_did, + |bytes| node_identity.sign(bytes), + ) + .await + { + Ok(_) => debug!( + "Periodic federation sync ok: {}", + node.did.chars().take(20).collect::() + ), + Err(e) => debug!( + "Periodic federation sync with {}: {}", + node.did.chars().take(20).collect::(), + e + ), + } + tokio::time::sleep(Duration::from_secs(5)).await; + } + } + }); + } + // Container health monitoring — auto-restart unhealthy containers // Respects webhook config: skips when disabled or ContainerCrash not subscribed crate::health_monitor::spawn_health_monitor(state_manager.clone(), config.data_dir.clone());