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:
UltradesuandClaude Opus 4.8 committed 2026-10-02 15:08:29 +01:00
1 parent 19509dc833
commit d1de61ba29
7 files changed
+416 -75

No files matched your search

+16 -1
View File
@@ -5,6 +5,7 @@ use std::time::Duration;
use eframe::egui;
use crate::agent::{AgentClient, resolve_socket};
use crate::stats::Traffic;
use crate::tray::{Action, Tray};
use crate::ui::{self, JoinForm};
@@ -13,6 +14,11 @@ pub(crate) struct App {
agent: AgentClient,
tray: Tray,
join: JoinForm,
/// Traffic rates derived from successive snapshots.
traffic: Traffic,
/// The snapshot generation last folded into `traffic`, so each refresh is
/// sampled exactly once rather than on every repaint.
sampled: u64,
/// Set when the user chose Quit, so the next close actually exits instead
/// of hiding to the tray.
quitting: bool,
@@ -31,6 +37,8 @@ impl App {
agent,
tray,
join: JoinForm::default(),
traffic: Traffic::default(),
sampled: 0,
quitting: false,
})
}
@@ -58,7 +66,14 @@ impl eframe::App for App {
}
let snapshot = self.agent.snapshot();
ui::draw(ctx, &self.agent, &mut self.join, &snapshot);
// Fold each fresh status into the traffic rates exactly once.
if snapshot.generation != self.sampled
&& let (Some(Ok(report)), Some(at)) = (&snapshot.status, snapshot.at)
{
self.traffic.observe(report, at);
self.sampled = snapshot.generation;
}
ui::draw(ctx, &self.agent, &mut self.join, &snapshot, &self.traffic);
// Keep ticking so fresh status and tray actions are picked up even when
// nothing else requests a repaint.