diff --git a/containers/libreportal/frontend/core/setup/css/setup-wizard.css b/containers/libreportal/frontend/core/setup/css/setup-wizard.css
index d8378ed..1a49d08 100755
--- a/containers/libreportal/frontend/core/setup/css/setup-wizard.css
+++ b/containers/libreportal/frontend/core/setup/css/setup-wizard.css
@@ -1227,15 +1227,18 @@ body.setup-wizard-open {
letter-spacing: 0.02em;
vertical-align: middle;
}
+/* The wizard sits on a mid-blue glass panel, so the dark-on-light badge colours
+ these started as read as muddy grey. Light-on-dark instead: enough contrast
+ to be legible without shouting over the drive name. */
.setup-storage-badge-warn {
- background: rgba(224, 168, 0, 0.16);
- color: #b98900;
- border: 1px solid rgba(224, 168, 0, 0.35);
+ background: rgba(255, 190, 60, 0.20);
+ color: #ffd27a;
+ border: 1px solid rgba(255, 190, 60, 0.48);
}
.setup-storage-badge-bad {
- background: rgba(200, 60, 60, 0.14);
- color: #c04040;
- border: 1px solid rgba(200, 60, 60, 0.32);
+ background: rgba(255, 110, 110, 0.18);
+ color: #ffa3a3;
+ border: 1px solid rgba(255, 110, 110, 0.45);
}
.setup-storage-card em {
opacity: 0.85;
@@ -1306,3 +1309,14 @@ body.setup-wizard-open .eo-modal { z-index: 10000; }
}
.setup-storage-fstab-opt { display: flex; gap: 9px; align-items: center; cursor: pointer; }
.setup-storage-fstab-note { margin-top: 10px !important; font-size: 0.85em !important; opacity: 0.75 !important; }
+
+/* The system drive: ticked and non-interactive, because apps fall back to it
+ and it therefore cannot be deselected. Kept at full opacity — unlike a
+ refused drive, it is not a lesser option, it is the default one. */
+.setup-storage-locked { opacity: 1; cursor: default; }
+.setup-storage-locked input[type=checkbox] { cursor: default; }
+.setup-storage-badge-ok {
+ background: rgba(90, 220, 150, 0.18);
+ color: #86ecb6;
+ border: 1px solid rgba(90, 220, 150, 0.45);
+}
diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
index 99de880..a8d938e 100755
--- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js
+++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
@@ -28,6 +28,7 @@ class SetupWizard {
// Set by loadStorage() once the candidate scan comes back.
this.hasStorageCandidates = false;
this.storageCandidates = [];
+ this.storageSystem = null;
this.selectedStorage = [];
// Paths the user asked us to make permanent in /etc/fstab.
this.fstabWanted = [];
@@ -43,7 +44,10 @@ class SetupWizard {
_stepVisible(idx) {
const name = this.stepNames[idx];
if (name === 'Metrics') return this.installLevel === 'advanced';
- if (name === 'Storage') return this.hasStorageCandidates;
+ // Storage always shows now. Even with one disk it answers "where does my
+ // data actually go?", which is worth a step in a self-hosting product —
+ // and the system drive's own details are the interesting part there.
+ if (name === 'Storage') return true;
return true;
}
@@ -396,13 +400,12 @@ class SetupWizard {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
this.storageCandidates = Array.isArray(data.candidates) ? data.candidates : [];
+ this.storageSystem = data.system || null;
} catch (e) {
- console.log('[setup] storage scan unavailable, skipping the Storage step:', e.message);
+ console.log('[setup] storage scan unavailable:', e.message);
this.storageCandidates = [];
+ this.storageSystem = null;
}
- // A candidate that cannot work is still WORTH SHOWING (greyed, with the
- // reason) — "why isn't my drive listed?" is a support burden. But if every
- // candidate is unusable there is nothing to choose, so skip the step.
this.hasStorageCandidates = this.storageCandidates.some(c => c.verdict !== 'refuse');
this.totalSteps = this._effectiveTotalSteps();
this.renderStorage();
@@ -447,60 +450,60 @@ class SetupWizard {
return map[c.id] || c.message;
}
+ // One card, one line.
+ //
+ // The badge carries severity and Details carries the explanation, so the card
+ // itself stays a single row — listing every warning here pushed each drive to
+ // three lines and made the step tall for no gain.
+ //
+ // `locked` renders it ticked and non-interactive: the system drive is what
+ // apps fall back to, so it cannot be deselected. Details still works, which
+ // is the point of showing it at all on a single-disk box.
+ _storageCard(c, key, opts) {
+ const o = opts || {};
+ const refused = c.verdict === 'refuse';
+ const badge = o.locked
+ ? 'default'
+ : (refused ? 'can\u2019t be used'
+ : (c.verdict === 'warn' ? 'needs care' : ''));
+
+ const title = o.locked ? 'System disk' : c.path;
+
+ return `
+ `;
+ }
+
renderStorage() {
const list = this.container.querySelector('#sw-storage-list');
const note = this.container.querySelector('#sw-storage-note');
if (!list) return;
- if (!this.storageCandidates.length) {
- list.innerHTML = '';
- if (note) note.textContent = '';
- return;
+ let html = '';
+ if (this.storageSystem) {
+ html += this._storageCard(this.storageSystem, 'system', { locked: true });
}
-
- list.innerHTML = this.storageCandidates.map((c, i) => {
- const refused = c.verdict === 'refuse';
- const warned = c.verdict === 'warn';
- const id = `sw-storage-${i}`;
- const badge = refused
- ? 'can\u2019t be used'
- : (warned ? 'needs care' : '');
-
- // One line of plain facts, then at most two short flags. Everything else
- // is a click away rather than in the user's face.
- const flags = (c.checks || [])
- .filter(k => k.severity === 'refuse' || k.severity === 'warn')
- .map(k => this._storageCheckSummary(k));
- const shown = flags.slice(0, 2);
- const more = flags.length - shown.length;
-
- return `
- `;
- }).join('');
+ html += this.storageCandidates.map((c, i) => this._storageCard(c, String(i), {})).join('');
+ list.innerHTML = html;
list.querySelectorAll('[data-storage-details]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
- this.showStorageDetails(Number(btn.dataset.storageDetails));
+ this.showStorageDetails(btn.dataset.storageDetails);
});
});
if (note) {
- note.innerHTML = 'A drive you tick becomes a storage location during install. '
- + 'Drives that can\u2019t hold app data are shown greyed with the reason.';
+ note.innerHTML = this.storageCandidates.length
+ ? 'Apps go on the system disk unless you tick another drive. Drives that can\u2019t hold app data are greyed out \u2014 open Details to see why.'
+ : 'No other drives were found, so everything goes on the system disk. Plug one in and you can add it later from the CLI (libreportal storage).';
}
}
@@ -512,13 +515,15 @@ class SetupWizard {
// document.body — a hand-rolled position:fixed backdrop appended INSIDE the
// wizard container resolves against the wizard's containing block (it has a
// backdrop-filter), so it covered part of the screen instead of all of it.
- showStorageDetails(idx) {
- const c = this.storageCandidates[idx];
+ showStorageDetails(key) {
+ const isSystem = key === 'system';
+ const c = isSystem ? this.storageSystem : this.storageCandidates[Number(key)];
if (!c) return;
const esc = (s) => this.escapeHtml(s);
const spec = [
- ['Mount point', c.path],
+ ...(isSystem ? [['Apps stored in', c.path], ['Mount point', c.mount || '/']]
+ : [['Mount point', c.path]]),
['Device', c.device],
['Filesystem', c.fstype],
['Size', c.size],
@@ -526,6 +531,7 @@ class SetupWizard {
['UUID', c.uuid || '—'],
['Mount options', c.options || '—'],
['Removable', c.removable ? 'Yes' : 'No'],
+ ...(isSystem && c.apps ? [['Apps here', c.apps]] : []),
].map(([k, v]) => `
${esc(v)}