git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit702ab74b74cff4c37822d0d6472c169c4fdb9f9a
parent54c45b69c2
authorLucas Galante <[email protected]>
date2026-08-14 12:56
fix: gate section_widgets on the load flag, so ids match what was painted

Registration is a side effect of `render_widget` during the view pass, and each
page's search box / backup button is painted only in the loaded branch — but
`section_widgets()` reported its id unconditionally. During the load window the
router therefore resolved a root that was never registered and dropped the event
with `unregistered/stale root ... event dropped`.

Live-confirmed on Packages, whose window is the longest of the three (`pacman -Q`
plus a `checkupdates` db sync): arriving on the page mid-load logged 18 such lines
and every pointer move added exactly one more; once the data landed, three further
moves added zero.

Each `section_widgets()` now mirrors its view's gate. The outer group count stays
the same either way — returning an empty outer Vec would empty `sec_focused` and
kill the ctrl-nav entry point.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/pages/packages.rs | 10 +++++++++-
 src/pages/services.rs |  9 ++++++++-
 src/pages/storage.rs  | 10 +++++++++-
 3 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/src/pages/packages.rs b/src/pages/packages.rs
index b36bb4b..478022a 100644
--- a/src/pages/packages.rs
+++ b/src/pages/packages.rs
@@ -716,7 +716,15 @@ impl PackagesState {
 impl crate::pages::AppPage for PackagesState {
     // Sections: [the one well]
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
-        vec![vec![self.search_box.id()]]
+        // Mirrors the view's `!loaded` early return: the search box is only painted
+        // (and so only registered) once `pacman -Q` + `checkupdates` land, which is
+        // the longest load window of any page. The group count stays 1 either way —
+        // an empty outer Vec would kill the ctrl-nav entry point.
+        if self.loaded {
+            vec![vec![self.search_box.id()]]
+        } else {
+            vec![Vec::new()]
+        }
     }
 
     fn view(
diff --git a/src/pages/services.rs b/src/pages/services.rs
index fbd4be1..0891ec6 100644
--- a/src/pages/services.rs
+++ b/src/pages/services.rs
@@ -372,7 +372,14 @@ fn service_action(name: &str, action: &str, is_system: bool) {
 impl crate::pages::AppPage for ServicesState {
     // Sections: [Services]
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
-        vec![vec![self.search_box.id()]]
+        // Mirrors the view's `!loaded` branch: the search box is only painted (and
+        // so only registered) once the unit list has landed. The group count stays
+        // 1 either way — an empty outer Vec would kill the ctrl-nav entry point.
+        if self.loaded {
+            vec![vec![self.search_box.id()]]
+        } else {
+            vec![Vec::new()]
+        }
     }
 
     fn view(
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index c5aaa7c..7ca7f43 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -354,7 +354,15 @@ impl crate::pages::AppPage for StorageState {
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
         // Only the third section (Full System Backup) has anything focusable;
         // the first two are read-only readouts, so ctrl+i there has no target.
-        vec![Vec::new(), Vec::new(), vec![self.backup_button.id()]]
+        // Gated on `backup_loaded` to mirror the view: the button is only painted
+        // (and so only registered) in that branch, and an id reported here while
+        // unregistered is a dead root the router drops with a warning.
+        let backup = if self.backup_loaded {
+            vec![self.backup_button.id()]
+        } else {
+            Vec::new()
+        };
+        vec![Vec::new(), Vec::new(), backup]
     }
 
     fn view(