The admin landing (overview) and the tools pages (ssh-access, system) call populateSidebar() without first loading window.configData. On a cold admin visit — e.g. navigating straight from the dashboard — configData is undefined, so populateSidebar() bails early and the sidebar renders empty. Visiting Backups happened to set window.configData, which is why returning to admin afterward showed the sidebar. Load (cached) config data up front in renderConfig before any branch renders so the sidebar always has its categories. The config-category path's later loadConfig is now a cache hit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
357 lines
16 KiB
JavaScript
Executable File
357 lines
16 KiB
JavaScript
Executable File
// Config Manager - Main orchestrator for modular config system
|
|
if (typeof window.ConfigManager === 'undefined') {
|
|
//console.log('ConfigManager: Defining new ConfigManager class...');
|
|
|
|
class ConfigManager {
|
|
constructor() {
|
|
this.core = new ConfigCore();
|
|
this.domainManager = new DomainManager();
|
|
this.whitelistManager = new IPWhitelistManager();
|
|
this.renderer = new ConfigRenderer();
|
|
this.sidebar = new ConfigSidebar();
|
|
this.form = new ConfigForm();
|
|
this.utils = new ConfigUtils();
|
|
|
|
// Expose IPWhitelistManager globally for wrapper functions
|
|
window.IPWhitelistManager = this.whitelistManager;
|
|
}
|
|
|
|
async renderConfig(category) {
|
|
//console.log('ConfigManager: Rendering ' + category + ' config...');
|
|
|
|
const configSection = document.getElementById('config-section');
|
|
if (!configSection) {
|
|
console.error('ConfigManager: config-section element not found');
|
|
return;
|
|
}
|
|
|
|
// The sidebar and page headers read window.configData. Load it up front so
|
|
// populateSidebar() has categories on a cold admin visit (e.g. straight
|
|
// from the dashboard); otherwise the overview/tools branches below render
|
|
// an empty sidebar until something else populates configData. Cached after
|
|
// the first call, so the config-category path below is a cache hit.
|
|
try { await this.core.loadConfig(category); } catch (e) {}
|
|
|
|
// Overview is the Admin landing — an ops/health board, not a config form.
|
|
if (category === 'overview') {
|
|
try { this.sidebar.populateSidebar(); } catch (e) {}
|
|
if (typeof AdminOverview !== 'undefined') {
|
|
window.adminOverview = new AdminOverview('config-section');
|
|
await window.adminOverview.init();
|
|
} else {
|
|
configSection.innerHTML = '<div class="error">Admin overview failed to load.</div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
// SSH Access is an admin tool page that lives in this sidebar rather than
|
|
// a config category — render its own controller into the main pane.
|
|
if (category === 'ssh-access') {
|
|
try { this.sidebar.populateSidebar(); } catch (e) {}
|
|
if (typeof SshPage !== 'undefined') {
|
|
window.sshPage = new SshPage('config-section');
|
|
await window.sshPage.init();
|
|
} else {
|
|
configSection.innerHTML = '<div class="error">SSH Access page failed to load.</div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
// System is an admin tool page (live host + per-app statistics) with its
|
|
// own controller, like SSH Access above.
|
|
if (category === 'system') {
|
|
try { this.sidebar.populateSidebar(); } catch (e) {}
|
|
if (typeof AdminSystem !== 'undefined') {
|
|
window.adminSystem = new AdminSystem('config-section');
|
|
await window.adminSystem.init();
|
|
} else {
|
|
configSection.innerHTML = '<div class="error">System page failed to load.</div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Show loading state with enhanced box styling
|
|
configSection.innerHTML = `
|
|
<div class="loading-content" style="
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
background: var(--input-bg);
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
|
width: calc(100% - 40px);
|
|
height: calc(100% - 40px);
|
|
box-sizing: border-box;
|
|
margin: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 400px;
|
|
">
|
|
<div class="loading-spinner" style="
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 3px solid rgba(52, 152, 219, 0.3);
|
|
border-top: 3px solid #3498db;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 16px auto;
|
|
display: inline-block;
|
|
"></div>
|
|
<div class="loading-message" style="
|
|
font-size: 16px;
|
|
color: #ffffff;
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
">
|
|
Loading configuration...
|
|
</div>
|
|
<div class="loading-subtitle" style="
|
|
font-size: 14px;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
font-style: italic;
|
|
">
|
|
${this.core.getRandomLoadingMessage()}
|
|
</div>
|
|
</div>
|
|
<style>
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
// Load configuration data
|
|
const configData = await this.core.loadConfig(category);
|
|
|
|
// Populate sidebar with categories
|
|
this.sidebar.populateSidebar();
|
|
|
|
if (Object.keys(configData).length === 0) {
|
|
configSection.innerHTML = '<div class="no-config"><h3>No Configuration Available</h3><p>No configuration items found for this category.</p></div>';
|
|
return;
|
|
}
|
|
|
|
// Render configuration sections
|
|
var formHTML = '';
|
|
var self = this; // Preserve 'this' context
|
|
|
|
// Features page is system-level — add a Danger Zone header at the
|
|
// top so it's visually obvious before the user touches anything.
|
|
// Reuses the same `.danger-zone-section` / `.danger-zone-header`
|
|
// styling used elsewhere, but without the advanced/unused toggle
|
|
// tickboxes that live inside the normal danger zone — this is just
|
|
// the heading.
|
|
if (category === 'features') {
|
|
formHTML += '<div class="danger-zone-section danger-zone-section--header-only"><div class="danger-zone-header"><h3>⚠️ Danger Zone</h3><p>These options are for advanced users and may affect system stability</p></div></div>';
|
|
// Divider below the features Danger Zone banner, separating it from
|
|
// the feature fields — same rule used elsewhere in the config form.
|
|
formHTML += '<div class="config-divider"></div>';
|
|
}
|
|
|
|
//console.log('ConfigManager: About to process configData entries:', Object.keys(configData));
|
|
|
|
// Filter subcategories by type
|
|
const subcategoryTypes = this.utils.filterSubcategoriesByType(configData, category);
|
|
|
|
// Render regular subcategories
|
|
for (const subcategoryName of subcategoryTypes.regular) {
|
|
const subcategoryData = configData[subcategoryName];
|
|
//console.log('ConfigManager: Processing regular subcategory:', subcategoryName, 'data:', subcategoryData);
|
|
|
|
if (typeof subcategoryData === 'object' && subcategoryData !== null) {
|
|
//console.log('ConfigManager: Calling renderSubcategory for:', subcategoryName);
|
|
formHTML += await self.renderSubcategory.call(self, category, subcategoryName, subcategoryData);
|
|
}
|
|
}
|
|
|
|
// Render advanced and unused sections
|
|
formHTML = await this.utils.renderSectionedContent(formHTML, subcategoryTypes.advanced, subcategoryTypes.unused, self, category, configData);
|
|
|
|
//console.log('ConfigManager: Final formHTML length:', formHTML.length);
|
|
|
|
if (formHTML) {
|
|
// Page-level header for the config section. Mirrors the
|
|
// .backup-page-header used on /backup so /config gets the same
|
|
// prominent H1 + description above the form fields. Looked up from
|
|
// window.configData.categories[category] so titles/descriptions
|
|
// come straight from the .category metadata file.
|
|
var catMeta = (window.configData && window.configData.categories && window.configData.categories[category]) || {};
|
|
var catTitle = catMeta.title || (typeof ConfigShared !== 'undefined' && ConfigShared.formatCategoryName ? ConfigShared.formatCategoryName(category) : category);
|
|
var catDesc = catMeta.description || '';
|
|
var catIcon = catMeta.icon || category;
|
|
var headerHTML =
|
|
'<div class="page-header config-page-header">' +
|
|
'<img class="page-header-icon" src="/icons/config/' + catIcon + '.svg" alt="" onerror="this.style.display=\'none\'">' +
|
|
'<div class="page-header-title">' +
|
|
'<div class="admin-breadcrumb">Config</div>' +
|
|
'<h1>' + catTitle + '</h1>' +
|
|
(catDesc ? '<p>' + catDesc + '</p>' : '') +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
configSection.innerHTML = headerHTML + '<form id="config-form" class="config-form">' + formHTML + '<div class="config-divider"></div><div class="config-actions">' +
|
|
'<button type="submit" class="btn btn-primary">' +
|
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
|
|
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>' +
|
|
'<polyline points="7 10 12 15 17 10"></polyline>' +
|
|
'<line x1="12" y1="15" x2="12" y2="3"></line>' +
|
|
'</svg>' +
|
|
'Save Configuration' +
|
|
'</button>' +
|
|
'<button type="button" class="btn btn-secondary" onclick="window.configManager.resetForm()">' +
|
|
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
'<polyline points="23 4 23 10 17 10"></polyline>' +
|
|
'<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>' +
|
|
'</svg>' +
|
|
'Reset' +
|
|
'</button>' +
|
|
'</div></form>';
|
|
// Wire the submit event so it dispatches the config-update task
|
|
// instead of letting the browser fall back to a GET that dumps every
|
|
// CFG value (including passwords) into the URL.
|
|
if (this.form && typeof this.form.attachSubmitHandler === 'function') {
|
|
this.form.attachSubmitHandler();
|
|
}
|
|
}
|
|
|
|
//console.log('ConfigManager: Successfully rendered ' + category + ' config');
|
|
|
|
// Force rediscover toggles to handle timing issues
|
|
if (window.toggleManager && window.toggleManager.forceRediscover) {
|
|
setTimeout(() => {
|
|
window.toggleManager.forceRediscover();
|
|
}, 200);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('ConfigManager: Error rendering ' + category + ' config: ', error);
|
|
configSection.innerHTML = '<div class="error-container"><h3>Error Loading Configuration</h3><p>Failed to load configuration: ' + error.message + '</p><button type="button" class="btn btn-primary" onclick="window.configManager.renderConfig(\'' + category + '\')">Retry</button></div>';
|
|
}
|
|
}
|
|
|
|
async renderSubcategory(category, subcategoryName, subcategoryData) {
|
|
//console.log('ConfigManager: renderSubcategory() called - category: ' + category + ', subcategory: ' + subcategoryName);
|
|
|
|
var displaySubcategory = this.utils.formatSubcategoryName(subcategoryName);
|
|
// Strip the parent-category prefix from the display title so the user
|
|
// sees "Basic" instead of "General Basic" while on the General page.
|
|
if (typeof ConfigShared !== 'undefined' && ConfigShared.stripCategoryPrefix) {
|
|
displaySubcategory = ConfigShared.stripCategoryPrefix(displaySubcategory, category);
|
|
}
|
|
var subcategoryDescription = this.utils.cleanDescription(subcategoryData.description || '');
|
|
|
|
// The subcategoryData IS the config items, not a container for them
|
|
var configItems = [];
|
|
|
|
// Look for actual config items in the main config object
|
|
if (window.configData && window.configData.config) {
|
|
Object.entries(window.configData.config).forEach(function([configKey, configValue]) {
|
|
if (configValue.subcategory === subcategoryName) {
|
|
configItems.push({
|
|
key: configKey,
|
|
title: configValue.title,
|
|
description: configValue.description,
|
|
value: configValue.value,
|
|
options: configValue.options,
|
|
master: configValue.master,
|
|
subcategory: configValue.subcategory
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
//console.log('ConfigManager: Processing subcategory:', subcategoryName, 'data:', subcategoryData);
|
|
//console.log('ConfigManager: configItems count: ' + configItems.length);
|
|
//console.log('ConfigManager: All config items keys:', configItems.map(item => item.key));
|
|
|
|
if (configItems.length === 0) {
|
|
//console.log('ConfigManager: No config items, returning empty string');
|
|
return '';
|
|
}
|
|
|
|
//console.log('ConfigManager: renderSubcategory called with:', {
|
|
//category,
|
|
//subcategoryName,
|
|
//displaySubcategory,
|
|
//hasData: !!subcategoryData
|
|
//});
|
|
|
|
// Check for master toggle in this subcategory
|
|
var masterKey = configItems.find(function(item) { return item.master === true; });
|
|
//console.log('ConfigManager: masterKey found: ' + !!masterKey, masterKey ? masterKey.key : null);
|
|
|
|
// Look for any ENABLED options and use universal toggle renderer
|
|
var enabledKey = configItems.find(function(item) {
|
|
//console.log('Checking item for ENABLED:', item.key, item.key.includes('ENABLED'));
|
|
return item.key.includes('ENABLED') || item.key === 'CFG_INSTALL_MODE';
|
|
});
|
|
//console.log('ConfigManager: enabledKey found: ' + !!enabledKey, enabledKey ? enabledKey.key : null);
|
|
|
|
// Special handling for domains section
|
|
var isDomains = subcategoryName.includes('domains') || subcategoryName.includes('network_domains');
|
|
//console.log('ConfigManager: isDomains:', isDomains);
|
|
|
|
// Special handling for IP whitelist section
|
|
var isWhitelist = subcategoryName === 'network_whitelist' || subcategoryName.includes('whitelist');
|
|
//console.log('ConfigManager: subcategoryName:', subcategoryName, 'isWhitelist:', isWhitelist);
|
|
|
|
var resultHTML = '';
|
|
|
|
if (isDomains) {
|
|
//console.log('ConfigManager: Using domains renderer');
|
|
// Render domains section with special handling
|
|
resultHTML = await this.domainManager.renderDomainsSection(configItems, displaySubcategory, subcategoryDescription);
|
|
} else if (isWhitelist) {
|
|
//console.log('ConfigManager: Using whitelist renderer');
|
|
resultHTML = await this.whitelistManager.renderWhitelistSection(configItems, displaySubcategory, subcategoryDescription);
|
|
} else if (enabledKey) {
|
|
//console.log('ConfigManager: Using universal toggle renderer');
|
|
// Use universal toggle renderer for any ENABLED option or CFG_INSTALL_MODE
|
|
resultHTML = window.toggleManager ? window.toggleManager.renderToggleSection(enabledKey, configItems, displaySubcategory, subcategoryDescription) : '';
|
|
} else if (masterKey) {
|
|
//console.log('ConfigManager: Using master toggle renderer');
|
|
// Render with master toggle
|
|
resultHTML = this.renderer.renderSubcategoryWithMaster(masterKey, configItems, displaySubcategory, subcategoryDescription);
|
|
} else {
|
|
//console.log('ConfigManager: Using regular renderer');
|
|
// Render regular subcategory
|
|
resultHTML = this.renderer.renderSubcategorySection(configItems, displaySubcategory, subcategoryDescription);
|
|
}
|
|
|
|
//console.log('ConfigManager: resultHTML length:', resultHTML.length);
|
|
return resultHTML;
|
|
}
|
|
|
|
// Delegate form operations to ConfigForm
|
|
resetForm() {
|
|
return this.form.resetForm();
|
|
}
|
|
|
|
// Domain management methods
|
|
addDomain() {
|
|
return window.domainManager.addDomain();
|
|
}
|
|
|
|
deleteDomain(domainKey, buttonElement) {
|
|
return window.domainManager.deleteDomain(domainKey, buttonElement);
|
|
}
|
|
|
|
async saveConfig() {
|
|
return await this.form.saveConfig();
|
|
}
|
|
|
|
showNotification(message, type) {
|
|
return this.form.showNotification(message, type);
|
|
}
|
|
}
|
|
|
|
// Export to global scope
|
|
window.ConfigManager = ConfigManager;
|
|
} else {
|
|
//console.log('ConfigManager: Already exists, using existing instance');
|
|
}
|