setup: registering a drive made it disappear from the Storage step

storage.json carries two lists: `candidates`, drives that could be added, and
`locations`, the ones already registered. The wizard read only the first. So a
drive vanished from Storage the moment it was registered — the step fell back to
"Only one drive found, so everything goes here" on a box with three, and because
the two root dropdowns only render when there is more than one option, the
choice they exist to offer disappeared with it.

A registered location is the clearest case of a usable drive there is. Read both
lists, deduplicated by path since one can appear in both while a registration
settles.

The generator's location entries carried no size or free figures either, so
those cards rendered as "free of" with both numbers missing next to a system
disk that had them. They now carry size, free, fstype and used_pct like the
system entry, and the card shows the name the user chose rather than the raw
path.

Found by the flow test: three registered locations, three apps placed across
them, and a Storage step insisting there was one drive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-28 13:09:43 +01:00
parent 65c947704b
commit 2d5674108b
2 changed files with 29 additions and 3 deletions

View File

@ -478,7 +478,19 @@ class SetupWizard {
const res = await fetch('/data/system/storage.json', { cache: 'no-store' }); const res = await fetch('/data/system/storage.json', { cache: 'no-store' });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); const data = await res.json();
this.storageCandidates = Array.isArray(data.candidates) ? data.candidates : []; // Both halves of the feed. `candidates` is drives that could be added;
// `locations` is the ones already registered — which the step showed as
// nothing at all, so registering a drive made it VANISH from Storage and
// the note went back to claiming only one drive was found. A registered
// location is the clearest case of a usable drive there is.
const registered = (Array.isArray(data.locations) ? data.locations : [])
.filter(l => l && l.path && l.path !== (data.primary || ''))
.map(l => Object.assign({ verdict: l.state === 'ok' ? 'ok' : 'warn', registered: true }, l));
const unregistered = Array.isArray(data.candidates) ? data.candidates : [];
// A path can legitimately appear in both while a registration settles.
const seen = new Set(registered.map(l => (l.path || '').replace(/\/$/, '')));
this.storageCandidates = registered.concat(
unregistered.filter(c => !seen.has((c.path || '').replace(/\/$/, ''))));
this.storageSystem = data.system || null; this.storageSystem = data.system || null;
} catch (e) { } catch (e) {
console.log('[setup] storage scan unavailable:', e.message); console.log('[setup] storage scan unavailable:', e.message);
@ -601,7 +613,9 @@ class SetupWizard {
: (refused ? '<span class="setup-storage-badge setup-storage-badge-bad">can\u2019t be used</span>' : (refused ? '<span class="setup-storage-badge setup-storage-badge-bad">can\u2019t be used</span>'
: (c.verdict === 'warn' ? '<span class="setup-storage-badge setup-storage-badge-warn">needs care</span>' : '')); : (c.verdict === 'warn' ? '<span class="setup-storage-badge setup-storage-badge-warn">needs care</span>' : ''));
const title = o.system ? this._primaryLabel() : c.path; // A registered location has a name the user chose; an unregistered
// candidate only has a path.
const title = o.system ? this._primaryLabel() : (c.name || c.path);
return ` return `
<div class="setup-app setup-storage-card${refused ? ' setup-storage-disabled' : ''} setup-storage-locked"> <div class="setup-app setup-storage-card${refused ? ' setup-storage-disabled' : ''} setup-storage-locked">

View File

@ -41,7 +41,19 @@ webuiGenerateStorageCandidates()
apps="$(storageAppsOnRoot "$path" 2>/dev/null | paste -sd, -)" apps="$(storageAppsOnRoot "$path" 2>/dev/null | paste -sd, -)"
(( first )) || locations+="," (( first )) || locations+=","
first=0 first=0
locations+="{\"id\":\"$(_lpJsonEsc "$id")\",\"name\":\"$(_lpJsonEsc "$name")\",\"path\":\"$(_lpJsonEsc "$path")\",\"state\":\"$(_lpJsonEsc "$state")\",\"apps\":\"$(_lpJsonEsc "$apps")\"}" # Size and free too. Without them the setup wizard renders a registered
# location as a card reading "free of" with both numbers missing — it
# shows the same card as the system disk, which does carry them.
local loc_size="" loc_free="" loc_fs="" loc_pct=""
if command -v findmnt >/dev/null 2>&1 && [[ -d "$path" ]]; then
loc_size=$(findmnt -no SIZE --target "$path" 2>/dev/null | tail -1)
loc_free=$(findmnt -no AVAIL --target "$path" 2>/dev/null | tail -1)
loc_fs=$(findmnt -no FSTYPE --target "$path" 2>/dev/null | tail -1)
fi
loc_pct=$(df -Pk "$path" 2>/dev/null | awk 'NR==2 {gsub("%","",$5); print $5}')
locations+="{\"id\":\"$(_lpJsonEsc "$id")\",\"name\":\"$(_lpJsonEsc "$name")\",\"path\":\"$(_lpJsonEsc "$path")\",\"state\":\"$(_lpJsonEsc "$state")\",\"apps\":\"$(_lpJsonEsc "$apps")\""
locations+=",\"size\":\"$(_lpJsonEsc "$loc_size")\",\"free\":\"$(_lpJsonEsc "$loc_free")\""
locations+=",\"fstype\":\"$(_lpJsonEsc "$loc_fs")\",\"used_pct\":${loc_pct:-0},\"removable\":false}"
done < <(runStorage verify 2>/dev/null) done < <(runStorage verify 2>/dev/null)
locations+="]" locations+="]"