Tray UI: start hidden, fixed window, grid layout, traffic + peers
Reworks the tray window per feedback and fills it in. - Starts hidden in the tray (no window on launch); the window opens from the menu. On macOS the app now uses the Accessory activation policy, so it lives in the menu bar with no Dock icon (via a winit event-loop hook, pinned to eframe's winit). Fixed, non-resizable 360x600 window; the layout targets it. - Tidied layout: top bar (title + refresh), a bottom panel with the join form and the action banner, and a scrolling list of network tiles between them. Labels and values sit on two-column grids so they align; the join name and secret inputs are the same fixed width. - A ⟳ button beside the secret generates a fresh random secret (NetworkSecret::generate), for creating a network. - Each network tile shows id, range, this device's address, peers (connected/known), the active and broadcast switches, a Leave button, and a collapsing "peers (N)" section with a formatted per-peer table (short key, address, handshake age, tx/rx packets, rate). - Traffic: a new sampler turns the agent's cumulative packet counters into a packets-per-second rate per network and per peer, from the deltas between snapshots (the snapshot now carries a generation and timestamp so each refresh is sampled once). The network tile shows the current rate and a small hand-drawn sparkline of recent rates (no extra dependency). Note: rates are packets/s — the report has no byte counters yet; bytes would need an agent change. Builds and is clippy-clean (-D warnings) on macOS; the UI has not been run in this headless session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
19509dc833
commit
d1de61ba29
7 files changed
+416
-75
No files matched your search
@@ -0,0 +1,89 @@
|
||||
//! Traffic rates derived from successive status snapshots.
|
||||
//!
|
||||
//! The agent reports cumulative packet counters, not rates. The GUI samples
|
||||
//! them each time a fresh snapshot arrives and turns the deltas into a
|
||||
//! packets-per-second rate plus a short rolling history for a sparkline. One
|
||||
//! series per key: a network total, and each peer within it.
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::time::Instant;
|
||||
|
||||
use tsunagi::ipc::StatusReport;
|
||||
|
||||
/// How many rate samples to keep for a sparkline.
|
||||
const HISTORY: usize = 48;
|
||||
|
||||
/// One series' rolling rate.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Series {
|
||||
last: Option<(Instant, u64)>,
|
||||
/// Most recent packets-per-second (tx + rx).
|
||||
pub rate: f32,
|
||||
/// Recent rates, oldest first, for a sparkline.
|
||||
pub history: VecDeque<f32>,
|
||||
}
|
||||
|
||||
impl Series {
|
||||
fn sample(&mut self, now: Instant, total: u64) {
|
||||
if let Some((then, previous)) = self.last {
|
||||
let dt = now.saturating_duration_since(then).as_secs_f32();
|
||||
if dt > 0.0 {
|
||||
let delta = total.saturating_sub(previous) as f32;
|
||||
self.rate = delta / dt;
|
||||
self.history.push_back(self.rate);
|
||||
while self.history.len() > HISTORY {
|
||||
self.history.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.last = Some((now, total));
|
||||
}
|
||||
}
|
||||
|
||||
/// All traffic series, keyed by a stable string.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Traffic {
|
||||
series: HashMap<String, Series>,
|
||||
}
|
||||
|
||||
impl Traffic {
|
||||
/// Folds a fresh snapshot into the rates. Call once per new generation.
|
||||
pub(crate) fn observe(&mut self, report: &StatusReport, now: Instant) {
|
||||
for network in &report.networks {
|
||||
let Some(overlay) = &network.overlay else {
|
||||
continue;
|
||||
};
|
||||
let mut net_total = 0u64;
|
||||
for peer in &overlay.peers {
|
||||
let total = peer.tx_packets.saturating_add(peer.rx_packets);
|
||||
net_total = net_total.saturating_add(total);
|
||||
self.series
|
||||
.entry(peer_key(&network.network_id, &peer.public_key))
|
||||
.or_default()
|
||||
.sample(now, total);
|
||||
}
|
||||
self.series
|
||||
.entry(net_key(&network.network_id))
|
||||
.or_default()
|
||||
.sample(now, net_total);
|
||||
}
|
||||
}
|
||||
|
||||
/// The network-total series, if sampled.
|
||||
pub(crate) fn network(&self, network_id: &str) -> Option<&Series> {
|
||||
self.series.get(&net_key(network_id))
|
||||
}
|
||||
|
||||
/// One peer's series, if sampled.
|
||||
pub(crate) fn peer(&self, network_id: &str, public_key: &str) -> Option<&Series> {
|
||||
self.series.get(&peer_key(network_id, public_key))
|
||||
}
|
||||
}
|
||||
|
||||
fn net_key(network_id: &str) -> String {
|
||||
format!("net:{network_id}")
|
||||
}
|
||||
|
||||
fn peer_key(network_id: &str, public_key: &str) -> String {
|
||||
format!("peer:{network_id}:{public_key}")
|
||||
}
|
||||
Reference in new issue
Block a user