From 6f38a706e18e655b4bc6e8791f328a7a0ee1556e Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Mon, 20 Jul 2026 18:43:25 +0200 Subject: [PATCH] feat: store hygiene - orphan cache pruning, description search, installed version - Doc/icon caches of repos that LEFT the catalog accumulated forever (nothing ever deleted them). Each successful catalog fetch now prunes cache directories whose repo is no longer listed - and only then: a cache fallback after a transient failure must never purge anything. Uninstalling a still-listed app deliberately keeps its caches (they render the catalog entry); it now drops the app's cached release notes, which described the removed version. - Search matched the repo name only: 'music' could not find Grape ('Lecteur musique...'). The filter now also matches the manifest display name and the description. - The installed version was recorded at install time but shown nowhere: the detail footer now displays it. --- src/i18n.rs | 2 ++ src/persistence.rs | 26 ++++++++++++++++++++++++++ src/state.rs | 4 ++++ src/ui/detail.rs | 18 ++++++++++++++++-- src/update.rs | 13 +++++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/i18n.rs b/src/i18n.rs index 91e8fe0..3ca16a5 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -701,6 +701,7 @@ impl Locale { strings.insert("update_all".into(), "Tout mettre à jour ({count})".into()); strings.insert("whats_new".into(), "Nouveautés de {version}".into()); strings.insert("view_on_github".into(), "Voir sur GitHub".into()); + strings.insert("installed_version".into(), "Installé : {version}".into()); strings.insert("launcher_update_system_managed".into(), "Mise à jour {version} disponible - cette installation est gérée par le gestionnaire de paquets, mettez à jour via « pacman -Syu » (colony-bin)".into()); // Detail tabs strings.insert("tab_readme".into(), "ReadMe".into()); @@ -1337,6 +1338,7 @@ impl Locale { strings.insert("update_all".into(), "Update all ({count})".into()); strings.insert("whats_new".into(), "What's new in {version}".into()); strings.insert("view_on_github".into(), "View on GitHub".into()); + strings.insert("installed_version".into(), "Installed: {version}".into()); strings.insert("launcher_update_system_managed".into(), "Update {version} is available - this install is managed by the package manager, update via 'pacman -Syu' (colony-bin)".into()); // Detail tabs strings.insert("tab_readme".into(), "ReadMe".into()); diff --git a/src/persistence.rs b/src/persistence.rs index 774429c..1ffeff1 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -259,6 +259,32 @@ pub struct CachedApp { pub origin: String, } +/// Remove doc/icon caches for repos that are NO LONGER in the catalog, so a +/// deleted or renamed repo does not leave its caches behind forever. Runs +/// after each successful catalog fetch (never on a cache fallback, where a +/// transient absence must not purge anything). Uninstalling a still-listed +/// app deliberately keeps its caches - they render the catalog entry. +pub fn prune_orphaned_repo_caches(live_repo_names: &[String]) { + let Ok(base) = colony_data_dir() else { + return; + }; + for parent in ["repo-docs", "repo-icons"] { + let Ok(entries) = std::fs::read_dir(base.join(parent)) else { + continue; + }; + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if !live_repo_names.iter().any(|r| r == &name) { + let path = entry.path(); + tracing::info!("pruning orphaned cache {}", path.display()); + if let Err(e) = std::fs::remove_dir_all(&path) { + tracing::warn!("failed to prune {}: {e}", path.display()); + } + } + } + } +} + /// Load the cached application scan (`None` if absent or unreadable). Read at /// boot when the startup scan is disabled, so the local-apps grid restores the /// last known state instead of showing "0 apps" at every launch. diff --git a/src/state.rs b/src/state.rs index 514cd15..bab24b3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -310,7 +310,11 @@ impl App { if query.is_empty() { return true; } + // Search the description and display name too: "music" must + // find Grape ("Lecteur musique..."), not just name matches. repo.name.to_lowercase().contains(&query) + || repo.manifest.name.to_lowercase().contains(&query) + || repo.description.to_lowercase().contains(&query) }) .collect() } diff --git a/src/ui/detail.rs b/src/ui/detail.rs index add3cf5..459916b 100644 --- a/src/ui/detail.rs +++ b/src/ui/detail.rs @@ -178,8 +178,22 @@ impl App { }) .collect(); - let mut footer_items: Vec> = - vec![container(text("")).width(Fill).into()]; + let mut footer_items: Vec> = Vec::new(); + // Store-basics metadata: the installed version was recorded at install + // time but never shown anywhere on the detail page. + if let Some(installed) = github::load_installed_version(&repo.name) { + footer_items.push( + text(crate::i18n::t_fmt( + "installed_version", + &[("version", &installed)], + )) + .size(self.sz(12)) + .font(self.app_font()) + .color(Palette::TEXT_DIM()) + .into(), + ); + } + footer_items.push(container(text("")).width(Fill).into()); for pt in platform_labels { footer_items.push(pt); } diff --git a/src/update.rs b/src/update.rs index 37d4f6e..e11b051 100644 --- a/src/update.rs +++ b/src/update.rs @@ -379,6 +379,14 @@ impl App { // The catalog is stored regardless of sign-in state: anonymous // fetches land here too. self.colony_repo_list = repos; + // A successful fetch is the one moment we KNOW which repos + // exist: drop doc/icon caches of repos that left the catalog. + let live: Vec = self + .colony_repo_list + .iter() + .map(|r| r.name.clone()) + .collect(); + crate::persistence::prune_orphaned_repo_caches(&live); // Decode any freshly-cached app icons into image handles. self.reload_app_icons(); // New docs may have landed for the repo currently viewed. @@ -624,6 +632,11 @@ impl App { self.confirm_uninstall = None; // An uninstalled app has no meaningful "update available". self.available_updates.remove(&repo_name); + // Stale notes describe the version that was just removed. + // (Doc/icon caches and the favorite deliberately survive: they + // belong to the CATALOG entry, which is still listed - orphan + // cleanup happens on catalog refresh instead.) + self.release_notes.remove(&repo_name); match github::colony_apps_dir() { Ok(apps_dir) => { let app_dir = apps_dir.join(&repo_name);