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:
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user