setup: surface the per-app storage choice in the App Center
Choosing a drive worked from the CLI but was invisible in the WebUI, for three
separate reasons, each of which hid the next:
* the config editor only renders fields listed in apps-field-mappings.json,
and STORAGE was not one — so no amount of correct data made it appear. Added
there, in General, with its choices built from the locations registered at
generate time (unlike every other select here, they are not knowable
statically).
* app TEMPLATES ship "[default:Primary]", and templates are what the install
form reads for an app that is not installed yet — precisely the app whose
form needs to show which drives exist. storageSyncAllAppComments now covers
templates, and is finally called from a regen path: it was written for one
and never wired in, so every option list was frozen at install time and
adding a drive made it selectable nowhere.
* storageLocationName resolved a name only from an in-scope
CFG_STORAGE_LOC_<id>_NAME and otherwise fell back to the bare id. That name
is the value CFG_<APP>_STORAGE takes, so the generated dropdown offered
"location-1" as both label and value — a choice that does not resolve. Read
it from the location's own config when the variable is not in scope.
Then the control rendered but sat blank. Config values are the raw right-hand
side of "KEY=value # comment"; almost all are stored without a comment, but a
field whose comment is regenerated keeps one — CFG_<APP>_STORAGE records the
location it currently resolves to. updateConfigForm assigned that whole string
to the field, which for a <select> matches no option, sets selectedIndex to -1
and renders empty: an app on a second disk read as "nothing configured", or
after a partial fix as "Primary". Normalise once where the config enters the
form, and never assign a select a value none of its options carry.
Verified in the App Center: authelia, installed on disk1, shows
"disk1 (/mnt/lptest1/apps)" selected, with Primary/disk1/disk2 offered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
56cd6e7fa4
commit
78bc20b8ac
@ -46,6 +46,18 @@ Object.assign(AppsManager.prototype, {
|
||||
});
|
||||
}
|
||||
},
|
||||
// Config values arrive as the raw right-hand side of "KEY=value # comment".
|
||||
// Most fields are written back without their comment, so this rarely
|
||||
// mattered — but any field whose comment is REGENERATED keeps one
|
||||
// (CFG_<APP>_STORAGE carries the location it currently resolves to), and then
|
||||
// a <select> gets a value matching none of its options and renders blank, as
|
||||
// if nothing were set. Strip it once here rather than per field.
|
||||
stripInlineComment(value) {
|
||||
if (typeof value !== 'string') return value;
|
||||
const cut = value.replace(/\s+#.*$/, '').trim();
|
||||
return cut.replace(/^"(.*)"$/, '$1');
|
||||
},
|
||||
|
||||
updateConfigForm(appName, appConfig) {
|
||||
const form = document.getElementById(`app-form-${appName}`);
|
||||
if (!form) return;
|
||||
@ -55,12 +67,20 @@ Object.assign(AppsManager.prototype, {
|
||||
Object.entries(appConfig).forEach(([key, value]) => {
|
||||
const field = form.querySelector(`[name="${key}"]`);
|
||||
if (!field) return;
|
||||
let nextValue = value;
|
||||
let nextValue = this.stripInlineComment(value);
|
||||
if (key.endsWith('_NETWORK')) {
|
||||
nextValue = this.applyContextualDefault('NETWORK', value, appData);
|
||||
nextValue = this.applyContextualDefault('NETWORK', nextValue, appData);
|
||||
}
|
||||
if (field.type === 'checkbox') {
|
||||
field.checked = nextValue === 'true' || nextValue === 'yes';
|
||||
} else if (field.tagName === 'SELECT') {
|
||||
// Assigning a value no option carries sets selectedIndex to -1, and the
|
||||
// control then renders blank — which reads as "nothing is configured"
|
||||
// rather than "the stored value is unrecognised". The renderer has
|
||||
// already fallen back to the field's default, so keep that instead.
|
||||
if ([...field.options].some(o => o.value === String(nextValue))) {
|
||||
field.value = nextValue;
|
||||
}
|
||||
} else {
|
||||
field.value = nextValue;
|
||||
}
|
||||
@ -74,6 +94,20 @@ Object.assign(AppsManager.prototype, {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalise once, here, instead of at each of the several places a value is
|
||||
// read. Config values are the raw right-hand side of "KEY=value # comment",
|
||||
// and most are stored without a comment — but any field whose comment is
|
||||
// regenerated keeps one (CFG_<APP>_STORAGE records the location it currently
|
||||
// resolves to). A <select> then matched no option and fell back to showing
|
||||
// the field's default, so an app living on a second disk read as "Primary".
|
||||
if (appData && appData.config) {
|
||||
appData = Object.assign({}, appData, {
|
||||
config: Object.fromEntries(
|
||||
Object.entries(appData.config).map(([k, v]) => [k, this.stripInlineComment(v)])
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
const cleanAppName = appData.command.split(' ').pop();
|
||||
|
||||
const requiresKey = Object.keys(appData.config || {}).find(k => k.endsWith('_REQUIRES_SERVICE'));
|
||||
@ -361,6 +395,7 @@ Object.assign(AppsManager.prototype, {
|
||||
|
||||
// Get current value or use default
|
||||
let fieldValue = cfgKey && appConfig.hasOwnProperty(cfgKey) ? appConfig[cfgKey] : (fieldConfig.default || '');
|
||||
fieldValue = this.stripInlineComment(fieldValue);
|
||||
fieldValue = this.applyContextualDefault(fieldKey, fieldValue, appData);
|
||||
const fieldHTML = await this.generateField(fieldKey, cfgKey, fieldValue, fieldConfig);
|
||||
if (fieldConfig.hideByDefault) {
|
||||
|
||||
@ -19,6 +19,28 @@ webuiCreateAppFieldMappings() {
|
||||
local temp_file="$(mktemp)"
|
||||
local final_file="${output_dir}/apps-field-mappings.json"
|
||||
|
||||
# Storage Location's choices are whatever is registered right now, so unlike
|
||||
# every other select here they cannot be written into the static block
|
||||
# below. Built once and substituted for the placeholder.
|
||||
#
|
||||
# This generator runs on the same regen that refreshes the storage view, so
|
||||
# adding a drive makes it selectable without a further step — which is what
|
||||
# was missing: the config file's own dropdown listed the locations, but the
|
||||
# WebUI's editor only renders fields named in THIS file, so the field was
|
||||
# invisible in the UI no matter what the config said.
|
||||
local storage_options='[{"value": "default", "label": "Primary"}]'
|
||||
if declare -f storageRoots >/dev/null 2>&1; then
|
||||
local _opts='[{"value": "default", "label": "Primary"}]' _root _name
|
||||
while IFS= read -r _root; do
|
||||
[[ -z "$_root" ]] && continue
|
||||
[[ "${_root%/}" == "$(primaryRoot 2>/dev/null)" ]] && continue
|
||||
_name=$(storageLocationName "$_root" 2>/dev/null) || continue
|
||||
[[ -z "$_name" || "$_name" == "default" ]] && continue
|
||||
_opts="${_opts%]}, {\"value\": \"${_name}\", \"label\": \"${_name} (${_root})\"}]"
|
||||
done < <(storageRoots 2>/dev/null)
|
||||
storage_options="$_opts"
|
||||
fi
|
||||
|
||||
# Generate the complete JSON in one go
|
||||
cat > "$temp_file" << 'JSONEOF'
|
||||
{
|
||||
@ -88,6 +110,7 @@ webuiCreateAppFieldMappings() {
|
||||
},
|
||||
JSONEOF
|
||||
|
||||
|
||||
# Add PORT_1 through PORT_20 dynamically
|
||||
for i in {1..20}; do
|
||||
cat >> "$temp_file" << PORTEOF
|
||||
@ -170,6 +193,14 @@ PORTEOF
|
||||
],
|
||||
"default": "auto"
|
||||
},
|
||||
"STORAGE": {
|
||||
"category": "general",
|
||||
"label": "Storage Location",
|
||||
"type": "select",
|
||||
"tooltip": "Which drive this app's data lives on. Apps can each use a different one.",
|
||||
"options": __STORAGE_OPTIONS__,
|
||||
"default": "default"
|
||||
},
|
||||
"UPDATE_TYPE": {
|
||||
"category": "general",
|
||||
"label": "Updates",
|
||||
@ -999,12 +1030,24 @@ RESTEOF
|
||||
|
||||
# Substitute live-runtime placeholders the heredoc can't compute
|
||||
# (single-quoted heredoc → no $var expansion). Currently:
|
||||
# __INSTALL_NAME__ → CFG_INSTALL_NAME
|
||||
# __INSTALL_NAME__ → CFG_INSTALL_NAME
|
||||
# __STORAGE_OPTIONS__ → the storage locations registered right now
|
||||
if [ $? -eq 0 ]; then
|
||||
local install_name_safe
|
||||
install_name_safe=$(printf '%s' "${CFG_INSTALL_NAME:-LibrePortal}" \
|
||||
| sed -e 's/[\/&]/\\&/g' -e 's/"/\\"/g')
|
||||
sed -i "s|__INSTALL_NAME__|${install_name_safe}|g" "$temp_file"
|
||||
# awk, not sed: the replacement is JSON and contains characters sed
|
||||
# would treat as syntax. Never leave the placeholder behind — that is
|
||||
# invalid JSON and the whole config editor fails to load.
|
||||
# The default is assigned separately, not inline as ${x:-...}: that form
|
||||
# ends at the first unescaped } and the replacement is JSON, so the rest
|
||||
# of it leaked out as literal text and produced a stray "]}".
|
||||
local _opts_json="$storage_options"
|
||||
[[ -n "$_opts_json" ]] || _opts_json='[{"value": "default", "label": "Primary"}]'
|
||||
awk -v opts="$_opts_json" \
|
||||
'{ gsub(/__STORAGE_OPTIONS__/, opts); print }' "$temp_file" > "${temp_file}.s" \
|
||||
&& mv -f "${temp_file}.s" "$temp_file"
|
||||
runFileWrite "$final_file" < "$temp_file"; rm -f "$temp_file"
|
||||
else
|
||||
rm -f "$temp_file" 2>/dev/null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user