fix: prevent install buttons showing before first container scan

Added containers_scanned flag to StatusInfo in the data model. Starts
false, set to true after the first Podman scan completes (~15s after
boot). Marketplace now shows a shimmer "Checking..." indicator on app
buttons until the scan finishes, preventing users from accidentally
re-installing apps that are already present but not yet enumerated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 11:46:38 +00:00
parent 6f7e5bc034
commit 8df64df536
4 changed files with 42 additions and 2 deletions

View File

@@ -66,6 +66,10 @@ pub struct StatusInfo {
pub backup_progress: Option<f32>,
#[serde(rename = "update-progress")]
pub update_progress: Option<f32>,
/// True after the first container scan completes. Frontend should
/// not show install buttons until this is true.
#[serde(rename = "containers-scanned", default)]
pub containers_scanned: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -253,6 +257,7 @@ impl DataModel {
updated: false,
backup_progress: None,
update_progress: None,
containers_scanned: false,
},
lan_address: Some("http://localhost:8100".to_string()),
tor_address: None,

View File

@@ -417,16 +417,18 @@ async fn scan_and_update_packages(
let packages_changed = !packages.is_empty() && current_data.package_data != packages;
let tor_addr = docker_packages::read_tor_address("archipelago");
let tor_changed = tor_addr != current_data.server_info.tor_address;
let first_scan = !current_data.server_info.status_info.containers_scanned;
if packages_changed || tor_changed {
if packages_changed || tor_changed || first_scan {
let mut data = current_data;
if !packages.is_empty() {
data.package_data = packages;
}
data.server_info.tor_address = tor_addr.clone();
data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t));
data.server_info.status_info.containers_scanned = true;
state.update_data(data).await;
debug!("📦 State changed (packages={}, tor={}), broadcasting update", packages_changed, tor_changed);
debug!("📦 State changed (packages={}, tor={}, first_scan={}), broadcasting update", packages_changed, tor_changed, first_scan);
}
Ok(())

View File

@@ -37,6 +37,7 @@ export interface StatusInfo {
'updated': boolean
'backup-progress': number | null
'update-progress': number | null
'containers-scanned'?: boolean
}
export type UIMode = 'gamer' | 'easy' | 'chat'

View File

@@ -202,6 +202,20 @@
>
{{ t('common.launch') }}
</button>
<!-- Not yet scanned show loading indicator instead of install -->
<span
v-else-if="!containersScanned && (app.source === 'local' || app.dockerImage)"
class="flex-1 px-4 py-2 rounded-lg text-white/50 text-sm font-medium text-center cursor-default relative overflow-hidden"
>
<span class="marketplace-shimmer-bg"></span>
<span class="relative flex items-center justify-center gap-2">
<svg class="animate-spin h-3.5 w-3.5 opacity-60" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Checking...
</span>
</span>
<button
v-else-if="!isInstalled(app.id) && (app.source === 'local' || app.dockerImage)"
data-controller-install-btn
@@ -505,6 +519,10 @@ const installedPackages = computed(() => {
return store.data?.['package-data'] || {}
})
const containersScanned = computed(() => {
return store.data?.['server-info']?.['status-info']?.['containers-scanned'] ?? false
})
// Function to categorize community apps based on their ID and description
function categorizeCommunityApp(app: MarketplaceApp): string {
// If app already has a category set, use it
@@ -1219,6 +1237,20 @@ function handleImageError(event: Event) {
</script>
<style scoped>
.marketplace-shimmer-bg {
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 100%);
background-size: 200% 100%;
animation: shimmer 2s ease-in-out infinite;
border-radius: inherit;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;