diff --git a/python/extensions/banners/_30_system_resources.py b/python/extensions/banners/_30_system_resources.py
index 51e764d63..9e1c257ef 100644
--- a/python/extensions/banners/_30_system_resources.py
+++ b/python/extensions/banners/_30_system_resources.py
@@ -10,15 +10,25 @@ class SystemResourcesCheck(Extension):
except Exception:
cpu_percent = None
+ try:
+ cpu_cores = psutil.cpu_count(logical=True)
+ except Exception:
+ cpu_cores = None
+
load_avg = self._get_load_average()
try:
vm = psutil.virtual_memory()
ram_percent = vm.percent
+ ram_used_gb = vm.used / (1024 ** 3)
+ ram_total_gb = vm.total / (1024 ** 3)
except Exception:
ram_percent = None
- disk_percent, disk_path = self._get_disk_usage_percent()
+ ram_used_gb = None
+ ram_total_gb = None
+
+ disk_percent, disk_used_gb, disk_total_gb, disk_path = self._get_disk_usage()
try:
net = psutil.net_io_counters()
@@ -33,12 +43,21 @@ class SystemResourcesCheck(Extension):
la1, la5, la15 = load_avg
load_value = f"{la1:.2f} / {la5:.2f} / {la15:.2f}"
- disk_value = "N/A"
- if disk_percent is not None:
- disk_value = f"{disk_percent:.0f}% ({disk_path})"
+ if disk_percent is None or disk_used_gb is None or disk_total_gb is None:
+ disk_value = "N/A"
+ else:
+ disk_value = f"{disk_used_gb:.2f}/{disk_total_gb:.2f} GB"
- cpu_value = "N/A" if cpu_percent is None else f"{cpu_percent:.0f}%"
- ram_value = "N/A" if ram_percent is None else f"{ram_percent:.0f}%"
+ if cpu_percent is None:
+ cpu_value = "N/A"
+ else:
+ cores_value = "" if cpu_cores is None else f" ({cpu_cores} cores)"
+ cpu_value = f"{cpu_percent:.0f}%{cores_value}"
+
+ if ram_percent is None or ram_used_gb is None or ram_total_gb is None:
+ ram_value = "N/A"
+ else:
+ ram_value = f"{ram_used_gb:.2f}/{ram_total_gb:.2f} GB"
cpu_bar = self._bar_html(cpu_percent)
ram_bar = self._bar_html(ram_percent)
@@ -50,20 +69,32 @@ class SystemResourcesCheck(Extension):
"priority": 10,
"title": "System Resources",
"html": (
- "
"
- "
"
- f"
CPU
"
- f"
"
- f"
RAM
"
- f"
"
- f"
Disk
"
- f"
"
+ "
"
+ "
"
+ "
"
+ "
CPU
"
+ f"
{cpu_value}
"
+ "
"
+ f"{cpu_bar}"
+ "
"
+ "
RAM
"
+ f"
{ram_value}
"
+ "
"
+ f"{ram_bar}"
+ "
"
+ "
Disk
"
+ f"
{disk_value}
"
+ "
"
+ f"{disk_bar}"
+ "
"
+ "
"
+ "
"
+ f"
Load (1/5/15)
{load_value}
"
+ f"
Net (since boot)
{net_sent} sent / {net_recv} recv
"
"
"
- f"
Load (1/5/15)
{load_value}
"
- f"
Net (since boot)
{net_sent} sent / {net_recv} recv
"
"
"
),
- "dismissible": False,
+ "dismissible": True,
"source": "backend",
})
@@ -80,8 +111,8 @@ class SystemResourcesCheck(Extension):
color = "#22c55e"
return (
- "
"
+ "
"
)
@@ -92,14 +123,16 @@ class SystemResourcesCheck(Extension):
except Exception:
return None
- def _get_disk_usage_percent(self) -> tuple[float | None, str]:
+ def _get_disk_usage(self) -> tuple[float | None, float | None, float | None, str]:
for path in ["/", os.path.expanduser("~")]:
try:
usage = psutil.disk_usage(path)
- return usage.percent, path
+ used_gb = usage.used / (1024 ** 3)
+ total_gb = usage.total / (1024 ** 3)
+ return usage.percent, used_gb, total_gb, path
except Exception:
continue
- return None, "/"
+ return None, None, None, "/"
def _format_bytes(self, value: int) -> str:
size = float(value)
diff --git a/webui/components/welcome/welcome-screen.html b/webui/components/welcome/welcome-screen.html
index 1364c57c1..5e07ed8f4 100644
--- a/webui/components/welcome/welcome-screen.html
+++ b/webui/components/welcome/welcome-screen.html
@@ -50,6 +50,25 @@
+
+
@@ -176,6 +195,53 @@
margin: 1.5rem 0;
}
+ .welcome-actions-footer {
+ max-width: 640px;
+ width: 100%;
+ display: flex;
+ justify-content: flex-start;
+ gap: 0.5rem;
+ margin-top: -0.75rem;
+ }
+
+ .welcome-refresh-button {
+ width: 38px;
+ height: 38px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: 1px solid var(--color-border);
+ border-radius: 10px;
+ color: var(--color-secondary);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .welcome-refresh-button:hover {
+ border-color: var(--color-primary);
+ background: var(--color-message-bg);
+ color: var(--color-text);
+ }
+
+ .welcome-refresh-button:disabled {
+ opacity: 0.6;
+ cursor: default;
+ }
+
+ .welcome-refresh-button .material-symbols-outlined {
+ font-size: 1.25rem;
+ }
+
+ .welcome-spin {
+ animation: welcome-spin 0.9s linear infinite;
+ }
+
+ @keyframes welcome-spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+
.welcome-action-card {
background: var(--color-panel);
border: 1px solid var(--color-border);
diff --git a/webui/components/welcome/welcome-store.js b/webui/components/welcome/welcome-store.js
index 7629b584d..003778bea 100644
--- a/webui/components/welcome/welcome-store.js
+++ b/webui/components/welcome/welcome-store.js
@@ -12,6 +12,7 @@ const model = {
banners: [],
bannersLoading: false,
lastBannerRefresh: 0,
+ hasDismissedBanners: false,
init() {
// Initialize visibility based on current context
@@ -107,9 +108,9 @@ const model = {
},
// Refresh banners: frontend checks → backend checks → merge
- async refreshBanners() {
+ async refreshBanners(force = false) {
const now = Date.now();
- if (now - this.lastBannerRefresh < 1000) return;
+ if (!force && now - this.lastBannerRefresh < 1000) return;
this.lastBannerRefresh = now;
this.bannersLoading = true;
@@ -117,10 +118,20 @@ const model = {
const frontendContext = this.buildFrontendContext();
const frontendBanners = this.runFrontendBannerChecks();
const backendBanners = await this.runBackendBannerChecks(frontendBanners, frontendContext);
+
+ const dismissed = this.getDismissedBannerIds();
+ const loadIds = new Set(
+ [...frontendBanners, ...backendBanners]
+ .filter(b => b?.id && b.dismissible !== false)
+ .map(b => b.id)
+ );
+ this.hasDismissedBanners = Array.from(loadIds).some(id => dismissed.has(id));
+
this.banners = this.mergeBanners(frontendBanners, backendBanners);
} catch (error) {
console.error("Failed to refresh banners:", error);
this.banners = this.runFrontendBannerChecks();
+ this.hasDismissedBanners = false;
} finally {
this.bannersLoading = false;
}
@@ -151,6 +162,15 @@ const model = {
dismissed.push(bannerId);
storage.setItem('dismissed_banners', JSON.stringify(dismissed));
}
+
+ this.hasDismissedBanners = this.getDismissedBannerIds().size > 0;
+ },
+
+ undismissBanners() {
+ localStorage.removeItem("dismissed_banners");
+ sessionStorage.removeItem("dismissed_banners");
+ this.hasDismissedBanners = false;
+ this.refreshBanners(true);
},
getBannerClass(type) {