feat(client): print user-facing diagnostics

The PLAN calls out client diagnostics that a user can read directly, not only
machine-shaped counters. The Windows client already built ClientDiagnostics
snapshots, but it printed a dense status line and left the UserDiagnostic model
unused.

Derive user-facing diagnostics from ClientDiagnostics in lanparty-obs so a
future GUI and the current CLI can share the same status vocabulary. The
messages report the states the runtime actually observes: relay reachability,
LAN gateway presence, TAP IP presence, and observed broadcast traffic. TAP IPs
are only described as DHCP when they are non-link-local IPv4 addresses, because
link-local IPv4 and IPv6 addresses do not prove DHCP success.

The client now prints those user-facing lines after the existing detailed
counter line. Gateway latency is intentionally not reported here; the current
protocol does not measure gateway RTT.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-obs -p lanparty-client-win
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check
- git diff --cached --check

Refs: PLAN.md Logging / diagnostics
This commit is contained in:
2026-05-21 22:22:43 +02:00
parent 881dee5491
commit 733badd2a8
3 changed files with 154 additions and 5 deletions
+39 -1
View File
@@ -22,7 +22,9 @@ use lanparty_client_route::{
use lanparty_client_tap::TapAdapter;
use lanparty_ctrl::{ControlMessage, PeerInfo, Role, RoomCode};
use lanparty_net::RelayEndpoint;
use lanparty_obs::{ClientDiagnostics, RelayDiagnostics, TapDiagnostics};
use lanparty_obs::{
ClientDiagnostics, RelayDiagnostics, TapDiagnostics, UserDiagnostic, UserDiagnosticLevel,
};
use lanparty_proto::MacAddr;
#[cfg(windows)]
@@ -438,6 +440,9 @@ fn tap_diagnostics_with_ip(base: &TapDiagnostics, ip: Option<IpAddr>) -> TapDiag
fn print_client_diagnostics(diagnostics: &ClientDiagnostics) {
println!("{}", format_client_diagnostics(diagnostics));
for diagnostic in diagnostics.user_diagnostics() {
println!("{}", format_user_diagnostic(&diagnostic));
}
}
async fn print_and_report_client_diagnostics(
@@ -482,6 +487,14 @@ fn optional_label<T: std::fmt::Display>(value: Option<T>) -> String {
value.map_or_else(|| "unknown".to_string(), |value| value.to_string())
}
fn format_user_diagnostic(diagnostic: &UserDiagnostic) -> String {
match diagnostic.level() {
UserDiagnosticLevel::Info => diagnostic.message().to_owned(),
UserDiagnosticLevel::Warning => format!("Warning: {}", diagnostic.message()),
UserDiagnosticLevel::Error => format!("Error: {}", diagnostic.message()),
}
}
async fn run_control_event_log(
session: &ClientSession,
relay_status: ClientRelayStatus,
@@ -887,6 +900,31 @@ mod tests {
);
}
#[test]
fn formats_user_diagnostic_levels() {
assert_eq!(
format_user_diagnostic(&UserDiagnostic::new(
UserDiagnosticLevel::Info,
"Connected to relay"
)),
"Connected to relay"
);
assert_eq!(
format_user_diagnostic(&UserDiagnostic::new(
UserDiagnosticLevel::Warning,
"Waiting for LAN gateway"
)),
"Warning: Waiting for LAN gateway"
);
assert_eq!(
format_user_diagnostic(&UserDiagnostic::new(
UserDiagnosticLevel::Error,
"Relay not reachable"
)),
"Error: Relay not reachable"
);
}
#[test]
fn refreshes_tap_diagnostics_ip_without_losing_static_fields() {
let base = TapDiagnostics::new(