diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js
index d739182d9..f82c993d3 100644
--- a/src/gui/src/UI/Dashboard/TabApps.js
+++ b/src/gui/src/UI/Dashboard/TabApps.js
@@ -491,6 +491,32 @@ const TabApps = {
async loadApps ($el_window) {
const $container = $el_window.find('.myapps-container');
+ // Subtle centered spinner, but only if the fetch is slow (>1.5s) so
+ // fast loads never flash it; once visible it stays for at least 1.5s
+ // so it doesn't blink out. Skipped on background refreshes — the
+ // current apps stay on screen while new data loads.
+ const SPINNER_DELAY = 1500;
+ const SPINNER_MIN_VISIBLE = 1500;
+ let spinnerShownAt = null;
+ const spinnerTimer = setTimeout(() => {
+ // Only cover an empty container; if tiles (or any state) are
+ // already on screen this is a background refresh — keep them.
+ if ( $container.children().length > 0 ) return;
+ spinnerShownAt = Date.now();
+ $container.html('
');
+ }, SPINNER_DELAY);
+
+ const finish = async (render) => {
+ clearTimeout(spinnerTimer);
+ if ( spinnerShownAt !== null ) {
+ const remaining = SPINNER_MIN_VISIBLE - (Date.now() - spinnerShownAt);
+ if ( remaining > 0 ) {
+ await new Promise(resolve => setTimeout(resolve, remaining));
+ }
+ }
+ render();
+ };
+
try {
// Fetch both APIs in parallel
const [installedRes, launchRes] = await Promise.all([
@@ -544,10 +570,12 @@ const TabApps = {
}
this._apps = merged;
- this.renderApps($el_window);
+ await finish(() => this.renderApps($el_window));
} catch (e) {
console.error('Failed to load installed apps:', e);
- $container.html('');
+ await finish(() => {
+ $container.html('');
+ });
}
},
diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css
index 26e4868b3..47ccda89a 100644
--- a/src/gui/src/css/dashboard.css
+++ b/src/gui/src/css/dashboard.css
@@ -747,6 +747,31 @@ input.myapps-search::-webkit-search-decoration {
margin: 0;
}
+.myapps-loading {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.myapps-spinner {
+ width: 28px;
+ height: 28px;
+ border: 3px solid var(--dashboard-border);
+ border-top-color: var(--dashboard-text-tertiary);
+ border-radius: 50%;
+ animation: myapps-spin 0.8s linear infinite, myapps-spinner-fade-in 0.3s ease-out;
+}
+
+@keyframes myapps-spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes myapps-spinner-fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
/* iOS-style pager: pages snap horizontally, dots below, hover arrows */
.myapps-pager {