diff --git a/containers/libreportal/frontend/core/setup/css/setup-wizard.css b/containers/libreportal/frontend/core/setup/css/setup-wizard.css
index b770ed9..3911eda 100755
--- a/containers/libreportal/frontend/core/setup/css/setup-wizard.css
+++ b/containers/libreportal/frontend/core/setup/css/setup-wizard.css
@@ -1322,3 +1322,25 @@ body.setup-wizard-open .eo-modal { z-index: 10000; }
color: #86ecb6;
border: 1px solid rgba(90, 220, 150, 0.45);
}
+
+/* Capacity meter on a drive card. Inline and small on purpose: it encodes the
+ proportion the text deliberately doesn't repeat, without adding a row.
+ Filled = free space, so the bar drains as the disk fills. */
+.setup-storage-meter {
+ display: inline-block;
+ vertical-align: middle;
+ width: 64px;
+ height: 5px;
+ margin-left: 10px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.20);
+ overflow: hidden;
+}
+.setup-storage-meter > span {
+ display: block;
+ height: 100%;
+ border-radius: 3px;
+ background: rgba(160, 226, 255, 0.85);
+}
+.setup-storage-meter-mid > span { background: rgba(255, 206, 110, 0.9); }
+.setup-storage-meter-high > span { background: rgba(255, 138, 138, 0.95); }
diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
index 298f812..8ec6cbb 100755
--- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js
+++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
@@ -448,6 +448,29 @@ class SetupWizard {
return map[c.id] || c.message;
}
+ // A thin capacity bar rather than a second number.
+ //
+ // Which figure matters depends on the question. For "will my data fit?" —
+ // the question this step actually asks — absolute free space decides it: a
+ // 4 GB disk that is 89% free is still useless for a media library.
+ // Percentage answers "is this filling up?", a health signal rather than a
+ // placement one. So the text carries the magnitude and the bar the
+ // proportion, which is what the eye reads fastest.
+ //
+ // The bar fills with FREE space, not used. Filling by usage meant a healthy
+ // 7%-full disk rendered as an almost-empty track that read as a broken
+ // widget, and it disagreed with the text right beside it. Filled = room to
+ // spare, draining = filling up, which is the same direction as the words.
+ _storageMeter(c) {
+ const used = Math.max(0, Math.min(100, Number(c.used_pct) || 0));
+ const free = 100 - used;
+ const level = free <= 10 ? ' setup-storage-meter-high'
+ : (free <= 25 ? ' setup-storage-meter-mid' : '');
+ return ``;
+ }
+
// One card, one line.
//
// The badge carries severity and Details carries the explanation, so the card
@@ -472,7 +495,10 @@ class SetupWizard {
${this.escapeHtml(title)} ${badge}
- ${this.escapeHtml(c.size)} · ${this.escapeHtml(c.free)} free · ${this.escapeHtml(c.fstype)}${c.removable ? ' · removable' : ''}
+
+ ${this.escapeHtml(c.free)} free of ${this.escapeHtml(c.size)}${c.removable ? ' · removable' : ''}
+ ${this._storageMeter(c)}
+
`;
diff --git a/scripts/webui/data/generators/system/webui_storage_candidates.sh b/scripts/webui/data/generators/system/webui_storage_candidates.sh
index b49b81d..c9327f2 100644
--- a/scripts/webui/data/generators/system/webui_storage_candidates.sh
+++ b/scripts/webui/data/generators/system/webui_storage_candidates.sh
@@ -70,6 +70,8 @@ webuiGenerateStorageCandidates()
system_json+=",\"free\":\"$(_lpJsonEsc "$sys_free")\""
system_json+=",\"uuid\":\"$(_lpJsonEsc "$sys_uuid")\""
system_json+=",\"options\":\"$(_lpJsonEsc "$sys_opts")\""
+ local sys_pct; sys_pct=$(df -Pk "$sys_root" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')
+ system_json+=",\"used_pct\":${sys_pct:-0}"
system_json+=",\"removable\":false"
system_json+=",\"apps\":\"$(_lpJsonEsc "$(storageAppsOnRoot "$sys_root" 2>/dev/null | paste -sd, -)")\""
system_json+=",\"checks\":$sys_checks}"
@@ -112,6 +114,8 @@ webuiGenerateStorageCandidates()
candidates+=",\"free\":\"$(_lpJsonEsc "$avail")\""
candidates+=",\"uuid\":\"$(_lpJsonEsc "$uuid_val")\""
candidates+=",\"options\":\"$(_lpJsonEsc "$opts_val")\""
+ local used_pct; used_pct=$(df -Pk "$target" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')
+ candidates+=",\"used_pct\":${used_pct:-0}"
candidates+=",\"removable\":$([[ "$rm_flag" == "1" ]] && echo true || echo false)"
candidates+=",\"verdict\":\"$verdict\""
candidates+=",\"fstab_line\":\"$(_lpJsonEsc "$fstab_line")\""