From e1017640854e03e64cbd5990ee0b11038c22c150 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 26 Aug 2026 02:38:14 +0100 Subject: [PATCH] feat(setup): drop the filesystem type, show capacity as free-of-total + a bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card said "911.9G · 808.4G free · ext4". The filesystem type is a Details row, not something you choose a drive on, so it goes. On percentage vs size: which one matters depends on the question. This step asks "will my data fit?", and absolute free space is what decides that — 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 ("808.4G free of 911.9G") and a thin bar carries the proportion, which is what the eye reads fastest, with no second number competing with the first. The bar fills with FREE space, not used. Filling by usage made a healthy 7%-full disk render as an almost-empty track that read as a broken widget — and it pointed the opposite way to the text beside it. Filled = room to spare, draining = filling up, matching the words. It turns amber below 25% free and red below 10%. Co-Authored-By: Claude Opus 5 --- .../frontend/core/setup/css/setup-wizard.css | 22 +++++++++++++++ .../frontend/core/setup/js/setup-wizard.js | 28 ++++++++++++++++++- .../system/webui_storage_candidates.sh | 4 +++ 3 files changed, 53 insertions(+), 1 deletion(-) 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")\""