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

commit525505f88272f2d1a5c6fb409f398b45c3abad84
parentea96841b8a
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 21:45
feat(system): Builds section listing installed apps' build times

The System page gains a Builds section: every compiled cce binary in
~/.local/bin with how long ago it was built, oldest first, so a client
left behind by a toolkit sweep stands out by its age alone.

ccebuild installs with plain `install`, which stamps the install time,
not the build time. When target/release still holds the matching build
(same size, not newer), its mtime is used instead; a newer uninstalled
build is flagged in red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/pages/system_info.rs | 159 ++++++++++++++++++++++++++++++++++++++++++++---
 src/pages/timers.rs      |   2 +-
 2 files changed, 152 insertions(+), 9 deletions(-)

diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 0e9cde8..4a1f40c 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -49,6 +49,7 @@ pub struct SystemState {
     pub cpu_cores: u32,
     pub gpus: Vec<String>,
     pub gpu_strings: Vec<String>,
+    pub builds: Vec<InstalledBuild>,
     pub cpu_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
     pub cpu_usage_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
     pub cpu_temp_label: cce_ui::widget::Adapted<cce_ui::widget::Label>,
@@ -97,6 +98,7 @@ impl Default for SystemState {
             cpu_cores: 0,
             gpus: Vec::new(),
             gpu_strings: Vec::new(),
+            builds: Vec::new(),
             cpu_label: Label::new("CPU Info"),
             cpu_usage_label: Label::new("CPU Usage"),
             cpu_temp_label: Label::new("CPU Temp"),
@@ -283,6 +285,91 @@ fn spawn_systemctl_force(action: &str) {
     let _ = crate::spawn_detached(cmd);
 }
 
+/// One compiled cce binary in `~/.local/bin` and when it was built.
+#[derive(Debug, Clone, PartialEq)]
+pub struct InstalledBuild {
+    pub name: String,
+    pub built: std::time::SystemTime,
+    /// `target/release` holds a newer build than the one installed — built
+    /// but never deployed, which is half of what `ccebuild status` reports.
+    pub newer_uninstalled: bool,
+}
+
+/// When the installed copy of a binary was built, and whether a newer build
+/// is waiting. Takes `(mtime, size)` of the installed file and, if present, of
+/// the one in `target/release`.
+///
+/// `ccebuild install` runs plain `install`, which does not preserve mtimes, so
+/// the installed file's own mtime is when it was INSTALLED. A whole-DE sweep
+/// installs long after the first crates finished building, so that is not the
+/// build time. When the build in `target/release` is no newer than the
+/// installed copy and the same size, it is the build that was installed, and
+/// its mtime is the real answer. Otherwise the build that was installed has
+/// since been overwritten (or never came from this tree), and the install
+/// time is the best bound left — the build can be no later than that.
+fn build_time(
+    installed: (std::time::SystemTime, u64),
+    built: Option<(std::time::SystemTime, u64)>,
+) -> (std::time::SystemTime, bool) {
+    match built {
+        Some((b_mtime, _)) if b_mtime > installed.0 => (installed.0, true),
+        Some((b_mtime, b_size)) if b_size == installed.1 => (b_mtime, false),
+        _ => (installed.0, false),
+    }
+}
+
+/// The workspace `ccebuild` last installed from, which it records for itself
+/// (`remember_workspace`). An app launched from the menu has no other way to
+/// find the tree.
+fn ccebuild_workspace() -> Option<std::path::PathBuf> {
+    let state = std::env::var_os("XDG_STATE_HOME")
+        .map(std::path::PathBuf::from)
+        .or_else(|| std::env::var_os("HOME").map(|h| std::path::Path::new(&h).join(".local/state")))?;
+    let ws = std::fs::read_to_string(state.join("cce/workspace")).ok()?;
+    let ws = std::path::PathBuf::from(ws.trim());
+    ws.is_dir().then_some(ws)
+}
+
+/// Every compiled `cce*` binary in `~/.local/bin`, oldest build first — the
+/// order that matters, since an app left behind by a toolkit sweep is stale
+/// against `cce-ui` in a way no built-vs-installed check can see, and its age
+/// is the only tell. Symlinks (`cce`, `cce-settings`) are aliases, and shell
+/// scripts (`ccebuild`, `cce-shadow`) have no build, so only ELF files count.
+fn scan_installed_builds() -> Vec<InstalledBuild> {
+    use std::io::Read;
+    let Some(home) = std::env::var_os("HOME") else { return Vec::new() };
+    let bindir = std::path::Path::new(&home).join(".local/bin");
+    let release = ccebuild_workspace().map(|ws| ws.join("target/release"));
+    let Ok(entries) = std::fs::read_dir(&bindir) else { return Vec::new() };
+
+    let stat = |p: &std::path::Path| -> Option<(std::time::SystemTime, u64)> {
+        let m = std::fs::metadata(p).ok()?;
+        Some((m.modified().ok()?, m.len()))
+    };
+
+    let mut builds: Vec<InstalledBuild> = entries
+        .filter_map(|e| e.ok())
+        .filter_map(|e| {
+            let name = e.file_name().into_string().ok()?;
+            if !name.starts_with("cce") || !e.file_type().ok()?.is_file() {
+                return None;
+            }
+            let path = e.path();
+            let mut magic = [0u8; 4];
+            std::fs::File::open(&path).ok()?.read_exact(&mut magic).ok()?;
+            if &magic != b"\x7fELF" {
+                return None;
+            }
+            let installed = stat(&path)?;
+            let built = release.as_ref().and_then(|r| stat(&r.join(&name)));
+            let (built, newer_uninstalled) = build_time(installed, built);
+            Some(InstalledBuild { name, built, newer_uninstalled })
+        })
+        .collect();
+    builds.sort_by(|a, b| a.built.cmp(&b.built).then_with(|| a.name.cmp(&b.name)));
+    builds
+}
+
 #[derive(Debug, Clone, Default)]
 pub struct SystemInfo {
     pub hostname: String,
@@ -293,6 +380,7 @@ pub struct SystemInfo {
     pub cpu_usage: f32,
     pub gpus: Vec<String>,
     pub gpu_strings: Vec<String>,
+    pub builds: Vec<InstalledBuild>,
 }
 
 pub async fn fetch_system_state() -> SystemInfo {
@@ -394,6 +482,7 @@ pub async fn fetch_system_state() -> SystemInfo {
         cpu_cores,
         gpus,
         gpu_strings,
+        builds: scan_installed_builds(),
     }
 }
 
@@ -407,10 +496,11 @@ const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
 pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_focused: bool, sec_focused: &[bool], layout: &mut dyn LayoutStrategy, _ctx: &mut cce_ui::context::UiContext) -> PageContent {
     let mut final_pc = PageContent::new();
     let sec_w = 320.0f32;
-    // Seven, matching the add_section calls below and the seven groups
+    // Six, matching the add_section calls below and the six groups
     // section_widgets reports. The count caps the grid's column count
-    // (`n.min(cols)`), so the stale 8 only bit once the window was wide enough
-    // for eight columns — harmless, but it read as a missing eighth section.
+    // (`n.min(cols)`), so a stale count only bites once the window is wide
+    // enough for that many columns — harmless, but it reads as a missing
+    // section.
     let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(6);
 
     // ── 1. System Section ──
@@ -563,10 +653,37 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
         });
     });
 
-    // ── 6. GPU Power Section ──
-
-
-
+    // ── 6. Builds Section ──
+    // When each installed cce binary was built. Oldest first: a client nobody
+    // rebuilt after a toolkit change is stale in a way `ccebuild status`
+    // cannot see, and sinks to the top here by its age alone.
+    builder.add_section(&mut final_pc, "Builds", sec_focused.get(5).copied().unwrap_or(false), |sec| {
+        if !state.loaded {
+            sec.text("Loading installed builds...", 12.0, 0.0, 12.0, TEXT_FG);
+            return;
+        }
+        if state.builds.is_empty() {
+            sec.text("No cce binaries in ~/.local/bin", 12.0, 0.0, 12.0, TEXT_DIM);
+            return;
+        }
+        let now = std::time::SystemTime::now();
+        let mut stack = sec.vstack(0.0);
+        for b in &state.builds {
+            let age = now.duration_since(b.built).map(|d| d.as_secs()).unwrap_or(0);
+            let (when, col) = if b.newer_uninstalled {
+                // Fits the column; the full story is in `build_time`.
+                (format!("{} ago · newer build", crate::pages::timers::humanize(age)), DANGER_BG)
+            } else {
+                (format!("{} ago", crate::pages::timers::humanize(age)), TEXT_DIM)
+            };
+            let name = b.name.clone();
+            stack.add_row(1, 0.0, 16.0, move |c, _, x, _| {
+                let y = c.ay();
+                c.pc.text(&name, x, y + 3.0, 11.0, TEXT_FG);
+                c.pc.text(&when, x + 140.0, y + 3.0, 11.0, col);
+            });
+        }
+    });
 
     final_pc
 }
@@ -584,6 +701,7 @@ pub fn update(state: &mut SystemState, msg: SystemMessage, ctx: &mut cce_ui::con
             state.cpu_cores = new.cpu_cores;
             state.gpus = new.gpus;
             state.gpu_strings = new.gpu_strings.clone();
+            state.builds = new.builds;
 
             if state.loaded {
                 state.hostname_label.set_text(&format!("{}  —  Linux {}", state.hostname, state.kernel));
@@ -675,7 +793,7 @@ pub fn update(state: &mut SystemState, msg: SystemMessage, ctx: &mut cce_ui::con
 
 
 impl crate::pages::AppPage for SystemState {
-    // Sections: [System, System Actions, CPU, GPU, System Files, Battery]
+    // Sections: [System, System Actions, CPU, GPU, System Files, Builds]
     fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
         vec![
             Vec::new(),
@@ -741,6 +859,31 @@ mod tests {
         assert!(parse_pending("==> system artifacts already up to date\n").is_empty());
     }
 
+    fn at(secs: u64) -> std::time::SystemTime {
+        std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)
+    }
+
+    /// The installed build is the one in target/release: report when it was
+    /// built, not when the sweep got around to installing it.
+    #[test]
+    fn build_time_prefers_the_matching_build() {
+        assert_eq!(build_time((at(200), 10), Some((at(100), 10))), (at(100), false));
+    }
+
+    /// A newer build that was never installed: the installed binary came from
+    /// an overwritten build, so the install time is the only bound left.
+    #[test]
+    fn build_time_flags_a_newer_uninstalled_build() {
+        assert_eq!(build_time((at(200), 10), Some((at(300), 12))), (at(200), true));
+    }
+
+    /// Older but a different size: not the build that was installed.
+    #[test]
+    fn build_time_ignores_a_build_that_does_not_match() {
+        assert_eq!(build_time((at(200), 10), Some((at(100), 12))), (at(200), false));
+        assert_eq!(build_time((at(200), 10), None), (at(200), false));
+    }
+
     #[test]
     fn test_view_layout_grid() {
         let mut state = SystemState::default();
diff --git a/src/pages/timers.rs b/src/pages/timers.rs
index 5697a2e..dff28f2 100644
--- a/src/pages/timers.rs
+++ b/src/pages/timers.rs
@@ -83,7 +83,7 @@ pub enum TimersMessage {
 const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
 
 /// "46min" / "3h" / "5d 3h" — coarse two-unit humanization.
-fn humanize(secs: u64) -> String {
+pub(crate) fn humanize(secs: u64) -> String {
     let (d, h, m) = (secs / 86_400, (secs % 86_400) / 3600, (secs % 3600) / 60);
     if d > 0 {
         if h > 0 { format!("{}d {}h", d, h) } else { format!("{}d", d) }