From 875c6673136c0ff17389e8e16e776a3e390ff266 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:13:32 +0200 Subject: [PATCH] fix(ui): render the connectivity status bar message properly --- .../src/components/status_bar.rs | 37 +++--- packages/oneclient_app/src/view/app/debug.rs | 114 ++++++++++++++++++ packages/oneclient_net/src/status.rs | 34 +++++- 3 files changed, 163 insertions(+), 22 deletions(-) diff --git a/packages/oneclient_app/src/components/status_bar.rs b/packages/oneclient_app/src/components/status_bar.rs index 89d33bd2..ecf4c9be 100644 --- a/packages/oneclient_app/src/components/status_bar.rs +++ b/packages/oneclient_app/src/components/status_bar.rs @@ -121,19 +121,6 @@ impl Component for StatusBanner { }); let p = intro.get().value(); - let content = rect() - .horizontal() - .cross_align(Alignment::Center) - .spacing(8.) - .child(Icon::new(issue.icon()).size(15.).color(Color::WHITE)) - .child( - label() - .text(issue.message()) - .font_size(12.) - .font_weight(FontWeight::MEDIUM) - .color(Color::WHITE), - ); - let close = issue.closeable().then(|| { rect() .position(Position::new_absolute().right(10.)) @@ -147,8 +134,12 @@ impl Component for StatusBanner { dismissed.write().insert(issue); }) .child(Icon::new(IconType::XClose).size(14.).color(Color::WHITE)) + .into_element() }); + // The icon+message row is laid out directly in the bar rather than in a + // nested auto-sized wrapper: a transparent rect with no explicit size + // measures to nothing here, which is what was swallowing the message. rect() .width(Size::window_percent(100.)) .height(Size::px(BAR_HEIGHT)) @@ -156,14 +147,20 @@ impl Component for StatusBanner { .layer(Layer::OverlayLevel(u8::MAX - 20)) .background(issue.background()) .opacity(p) + .horizontal() + .main_align(Alignment::Center) + .cross_align(Alignment::Center) + .spacing(8.) + .padding(Gaps::new_symmetric(0., 40.)) + .child(Icon::new(issue.icon()).size(15.).color(Color::WHITE)) .child( - rect() - .width(Size::fill()) - .height(Size::fill()) - .center() - .padding(Gaps::new_symmetric(0., 16.)) - .child(content) - .maybe_child(close), + label() + .text(issue.message()) + .font_size(12.) + .font_weight(FontWeight::MEDIUM) + .max_lines(1) + .color(Color::WHITE), ) + .maybe_child(close) } } diff --git a/packages/oneclient_app/src/view/app/debug.rs b/packages/oneclient_app/src/view/app/debug.rs index 7d757132..a358bafd 100644 --- a/packages/oneclient_app/src/view/app/debug.rs +++ b/packages/oneclient_app/src/view/app/debug.rs @@ -3,6 +3,7 @@ use freya::router::RouterContext; use oneclient_db::console::{ConsoleQueryResult, run_console_query}; use oneclient_auth::preview_samples; +use oneclient_net::status::{self, ServiceStatus}; use crate::components::{Button, Icon, IconType, TextInput, login_dialog, toggle}; use crate::hooks::use_dispatch; @@ -76,6 +77,11 @@ impl Component for Debug { vec![LauncherUpdateSimulator.into_element()], )) .child(divider()) + .child(section( + "Connectivity Status Bar", + vec![StatusBarSimulator.into_element()], + )) + .child(divider()) .child(section( "Auth Error Guidance", vec![AuthGuidancePreview.into_element()], @@ -468,6 +474,114 @@ impl Component for LauncherUpdateSimulator { } } +/// Pins `oneclient_net::status` so the connectivity banner can be looked at +/// without unplugging anything. Each preset is one banner the bar knows how to +/// draw; the bar itself picks the first active issue. +#[derive(PartialEq)] +struct StatusBarSimulator; + +const STATUS_PRESETS: [(&str, IconType, ServiceStatus); 4] = [ + ( + "No internet", + IconType::Globe01, + ServiceStatus { + online: false, + mc_auth_up: false, + polyfrost_up: false, + }, + ), + ( + "Minecraft auth down", + IconType::AlertTriangle, + ServiceStatus { + online: true, + mc_auth_up: false, + polyfrost_up: true, + }, + ), + ( + "Polyfrost down", + IconType::AlertCircle, + ServiceStatus { + online: true, + mc_auth_up: true, + polyfrost_up: false, + }, + ), + ( + "Both services down", + IconType::AlertTriangle, + ServiceStatus { + online: true, + mc_auth_up: false, + polyfrost_up: false, + }, + ), +]; + +impl Component for StatusBarSimulator { + fn render(&self) -> impl IntoElement { + let mut pinned = use_state(status::forced); + + let mut row = rect().horizontal().width(Size::fill()).spacing(12.); + for (text, icon, forced) in STATUS_PRESETS { + row = row.child( + Button::new() + .secondary() + .child(Icon::new(icon).size(16.)) + .text(text) + .on_press(move |_| { + status::force(Some(forced)); + pinned.set(Some(forced)); + }), + ); + } + + let current = *pinned.read(); + let state_text = match current { + Some(_) => "Pinned. Probing still runs, but its result is not published.".to_string(), + None => "Not pinned; showing the real probe result.".to_string(), + }; + + rect() + .vertical() + .width(Size::fill()) + .spacing(10.) + .child( + label() + .text("Forces what the connectivity prober reports, so the real status bar renders its banner. Dismissing a banner is remembered until the issue clears.") + .font_size(13.) + .color(colors::fg_secondary()), + ) + .child(row) + .child( + rect() + .horizontal() + .width(Size::fill()) + .cross_align(Alignment::Center) + .spacing(12.) + .child( + Button::new() + .primary() + .child(Icon::new(IconType::RefreshCw01).size(16.)) + .text("Clear + recheck") + .enabled(current.is_some()) + .on_press(move |_| { + status::force(None); + pinned.set(None); + }), + ) + .child( + label() + .text(state_text) + .font_size(13.) + .color(colors::fg_secondary()), + ), + ) + .into_element() + } +} + #[derive(PartialEq)] struct AuthGuidancePreview; diff --git a/packages/oneclient_net/src/status.rs b/packages/oneclient_net/src/status.rs index 15b2399d..535e6f4f 100644 --- a/packages/oneclient_net/src/status.rs +++ b/packages/oneclient_net/src/status.rs @@ -1,4 +1,4 @@ -use std::sync::OnceLock; +use std::sync::{OnceLock, RwLock}; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -34,6 +34,9 @@ struct Connectivity { rx: watch::Receiver, notify: Notify, started: AtomicBool, + /// Debug-only pin. While set, probe results are recorded but never + /// published, so the forced status survives a recheck. + forced: RwLock>, } fn connectivity() -> &'static Connectivity { @@ -45,10 +48,34 @@ fn connectivity() -> &'static Connectivity { rx, notify: Notify::new(), started: AtomicBool::new(false), + forced: RwLock::new(None), } }) } +/// Pins the reported status to `status`, or releases the pin with `None` and +/// re-probes. Exposed for the debug page: the connectivity banner is otherwise +/// only reachable by actually taking a service down. +pub fn force(status: Option) { + let conn = connectivity(); + *conn.forced.write().unwrap_or_else(|e| e.into_inner()) = status; + + match status { + Some(status) => { + let _ = conn.tx.send(status); + } + None => request_recheck(), + } +} + +#[must_use] +pub fn forced() -> Option { + *connectivity() + .forced + .read() + .unwrap_or_else(|e| e.into_inner()) +} + pub fn subscribe() -> watch::Receiver { connectivity().rx.clone() } @@ -78,7 +105,10 @@ pub fn start(client: RequestClient) { tokio::spawn(async move { loop { - let _ = conn.tx.send(check_service_status(&client).await); + let probed = check_service_status(&client).await; + if forced().is_none() { + let _ = conn.tx.send(probed); + } conn.notify.notified().await; } });