fix(webui): hide empty config tabs; rename backup 'Advanced' to 'Engine'

Empty tabs: generateSimpleTabsAndContent gated tabs on a hasFields heuristic
that drifted from what generateConfigFields actually emits, so a category like
Network could show a tab whose body only read "No configuration options
available". Render each category's fields first and emit the tab only when the
output is non-empty, keeping tabs and content in lockstep.

Rename: the backup_advanced subcategory now displays as "Engine" via a
display-name override in formatSubcategoryName. File and CFG_BACKUP_* keys are
unchanged, so saved values are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-22 23:46:12 +01:00
parent 06970b528c
commit c227c01969
2 changed files with 33 additions and 46 deletions

View File

@ -997,72 +997,52 @@ class AppsManager {
let tabsHTML = ''; let tabsHTML = '';
let contentHTML = ''; let contentHTML = '';
let firstTab = null;
// Sort categories by order // Sort categories by order
const sortedCategories = Object.entries(categories) const sortedCategories = Object.entries(categories)
.sort(([,a], [,b]) => a.order - b.order); .sort(([,a], [,b]) => a.order - b.order);
// Find first tab with fields // Render each category's fields up front and keep only the ones that
// actually produced fields. A "hasFields" heuristic used to gate the tabs
// separately, but it drifted from what generateConfigFields really emits —
// so a category like Network could pass the check yet render empty, leaving
// a blank tab whose body just reads "No configuration options available".
// Trusting the rendered output keeps tabs and content in lockstep.
const renderedCategories = [];
for (const [key, category] of sortedCategories) { for (const [key, category] of sortedCategories) {
const content = await this.generateConfigFields(key, appData);
const hasFields = Object.entries(fieldMappings).some(([fieldKey, fieldConfig]) => { if (!content || content.includes('class="no-fields"')) continue;
if (fieldConfig.category === key) { renderedCategories.push({ key, category, content });
const cfgKey = this.findMatchingCFGKey(fieldKey, appConfig);
return cfgKey && appConfig.hasOwnProperty(cfgKey);
}
return false;
});
if (hasFields) {
firstTab = key;
break;
}
} }
// Use preferred category if available and valid, otherwise use firstTab // Use the preferred category if it's one that has fields, else the first.
const activeTab = preferredCategory && categories[preferredCategory] ? preferredCategory : firstTab; const activeTab = (preferredCategory && renderedCategories.some(c => c.key === preferredCategory))
? preferredCategory
// Generate tabs and content together : (renderedCategories[0] ? renderedCategories[0].key : null);
for (const [key, category] of sortedCategories) {
const hasFields = Object.entries(fieldMappings).some(([fieldKey, fieldConfig]) => { for (const { key, category, content } of renderedCategories) {
if (fieldConfig.category === key) { const isActive = key === activeTab ? 'active' : '';
const cfgKey = this.findMatchingCFGKey(fieldKey, appConfig);
return cfgKey && appConfig.hasOwnProperty(cfgKey); tabsHTML += `
}
return false;
});
if (hasFields) {
const isActive = key === activeTab ? 'active' : '';
// Generate tab button
tabsHTML += `
<button class="tab-button ${isActive}" data-tab="${key}" onclick="appsManager.showTab('${key}')"> <button class="tab-button ${isActive}" data-tab="${key}" onclick="appsManager.showTab('${key}')">
<span class="tab-emoji">${category.icon}</span> <span class="tab-emoji">${category.icon}</span>
<span class="tab-name">${category.name}</span> <span class="tab-name">${category.name}</span>
</button> </button>
`; `;
// Generate content panel contentHTML += `
//// // console.log(`🔧 Generating content for category: ${key}`);
const categoryContent = await this.generateConfigFields(key, appData);
contentHTML += `
<div class="tab-panel ${isActive}" id="panel-${key}"> <div class="tab-panel ${isActive}" id="panel-${key}">
<div class="panel-header"> <div class="panel-header">
<h4>${category.icon} ${category.name}</h4> <h4>${category.icon} ${category.name}</h4>
<p>${category.description}</p> <p>${category.description}</p>
</div> </div>
<div class="panel-fields app-config"> <div class="panel-fields app-config">
${categoryContent} ${content}
</div> </div>
</div> </div>
`; `;
}
} }
//// // console.log('✅ Tabs and content generated successfully');
return { tabsHTML, contentHTML }; return { tabsHTML, contentHTML };
} }

View File

@ -5,6 +5,13 @@ class ConfigUtils {
} }
formatSubcategoryName(subcategoryName) { formatSubcategoryName(subcategoryName) {
// Display-name overrides where the derived title isn't specific enough.
// The underlying file/key names (e.g. backup_advanced, CFG_BACKUP_*) are
// untouched, so saved values are unaffected.
const overrides = {
backup_advanced: 'Engine'
};
if (overrides[subcategoryName]) return overrides[subcategoryName];
return subcategoryName.replace(/_/g, ' ').replace(/\b\w/g, function(l) { return l.toUpperCase(); }); return subcategoryName.replace(/_/g, ' ').replace(/\b\w/g, function(l) { return l.toUpperCase(); });
} }