librelad 875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00

292 lines
10 KiB
JavaScript
Executable File

// Configuration Router - Loads appropriate config component based on URL
class ConfigRouter {
constructor() {
this.currentComponent = null;
}
async init() {
//console.log('ConfigRouter: Initializing...');
// Get current category from query parameter or global variable
const searchParams = new URLSearchParams(window.location.search);
let currentCategory = searchParams.get('config') || window.configCategory || 'general';
//console.log(`Initial parsing - searchParams.get('config'): ${searchParams.get('config')}`);
//console.log(`Window location search: ${window.location.search}`);
//console.log(`Initial currentCategory: ${currentCategory}`);
// Handle the case where URL is config?=backup (malformed but common)
// Check if we got the category from URL params, not from fallback
const gotFromUrlParams = searchParams.get('config') !== null;
if (!gotFromUrlParams && window.location.search.includes('?=')) {
//console.log(`URL contains ?= and no valid config param, attempting regex match...`);
const pathMatch = window.location.search.match(/\?=([^&]+)/);
//console.log(`Regex match result:`, pathMatch);
//console.log(`Regex test result:`, /\?=([^&]+)/.test(window.location.search));
if (pathMatch && pathMatch[1]) {
currentCategory = pathMatch[1];
//console.log(`Updated currentCategory from regex: ${currentCategory}`);
} else {
//console.log(`Regex failed to match, currentCategory remains: ${currentCategory}`);
}
} else {
//console.log(`Got category from URL params (${gotFromUrlParams}) or URL doesn't contain ?=, keeping: ${currentCategory}`);
}
//console.log(`Config router init: final category=${currentCategory}`);
// Backup config moved to /backup — redirect old URL/bookmarks.
if (currentCategory === 'backup') {
if (window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') {
window.librePortalSPA.navigate('/backup', true);
} else {
window.location.href = '/backup';
}
return;
}
// Load categories for sidebar
await this.loadConfigCategories();
// Set active category in sidebar
this.setActiveCategory(currentCategory);
// Load appropriate config component
await this.loadConfigComponent(currentCategory);
}
async loadConfigCategories() {
try {
//console.log('Loading config categories from: data/config/generated/configs.json');
// Start loading bar
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(20);
}
// Load unified config file
const response = await fetch('/data/config/generated/configs.json');
//console.log('Response status:', response.status, response.statusText);
//console.log('Response ok:', response.ok);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const text = await response.text();
//console.log('Raw response text:', text);
//console.log('Text length:', text.length);
//console.log('First 100 chars:', text.substring(0, 100));
if (!text || text.trim() === '') {
throw new Error('Empty response from configs.json');
}
const data = JSON.parse(text);
const categories = data.categories;
//console.log('Parsed categories:', categories);
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(60);
}
const categoriesList = document.getElementById('config-categories-list');
if (!categoriesList) {
console.error('config-categories-list element not found');
return;
}
categoriesList.innerHTML = '';
// Convert categories object to array and sort by ORDER
const categoriesArray = Object.entries(categories).map(([key, value]) => ({
id: key,
...value
}));
// Sort by ORDER if available, otherwise by title
categoriesArray.sort((a, b) => {
const orderA = parseInt(a.order) || 999;
const orderB = parseInt(b.order) || 999;
return orderA - orderB;
});
categoriesArray.forEach(category => {
const categoryItem = document.createElement('div');
categoryItem.className = 'category';
categoryItem.setAttribute('data-category', category.id);
// Use correct icon from our new structure
const iconName = category.icon || category.id;
const iconPath = `/icons/config/${iconName}.svg`;
//console.log(`Category: ${category.id}, Icon path: ${iconPath}`);
categoryItem.innerHTML = `
<img src="${iconPath}" alt="${category.title}" style="width: 20px; height: 20px; margin-right: 8px;"/>
${category.title}
`;
categoryItem.addEventListener('click', () => {
//console.log(`Category clicked: ${category.id}`);
this.navigateToCategory(category.id);
});
categoriesList.appendChild(categoryItem);
});
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(80);
}
} catch (error) {
console.error('Error loading config categories:', error);
if (typeof router !== 'undefined' && router.hideLoadingBar) {
router.hideLoadingBar();
}
}
}
setActiveCategory(categoryId) {
// Update active state
document.querySelectorAll('.category').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-category="${categoryId}"]`)?.classList.add('active');
}
navigateToCategory(categoryId) {
//console.log(`Config router: navigating to ${categoryId} (SPA mode)`);
// Update URL without full page reload using query parameter
const url = `/config?=${categoryId}`;
//console.log(`Updating URL to: ${url}`);
window.history.pushState({}, '', url);
// Set active category
this.setActiveCategory(categoryId);
// Load config content dynamically
this.loadConfigComponent(categoryId);
}
async loadConfigComponent(categoryId) {
try {
//console.log(`Config router: Loading component for ${categoryId}`);
// Start loading bar
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(10);
router.showLoadingBar();
}
// Clear current content
const configSection = document.getElementById('config-section');
if (configSection) {
configSection.innerHTML = '<div class="loading">Loading configuration...</div>';
}
// Update progress
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(30);
}
// Use the simple config manager
if (window.configManager) {
//console.log(`Using ConfigManager for ${categoryId}`);
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(50);
}
await window.configManager.renderConfig(categoryId);
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(90);
}
} else {
// Fallback - try to load config manager
//console.log('ConfigManager not available, loading it...');
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(40);
}
await new Promise((resolve) => {
const script = document.createElement('script');
script.src = '/js/components/config/config-manager.js';
script.onload = async () => {
if (window.configManager) {
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(60);
}
await window.configManager.renderConfig(categoryId);
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(90);
}
}
resolve();
};
document.head.appendChild(script);
});
}
// Complete loading
if (typeof router !== 'undefined' && router.updateProgress) {
router.updateProgress(100);
setTimeout(() => {
router.hideLoadingBar();
}, 500); // Small delay to show completion
}
} catch (error) {
console.error(`Error loading config component for ${categoryId}:`, error);
const configSection = document.getElementById('config-section');
if (configSection) {
configSection.innerHTML = `<div class="error">Failed to load ${categoryId} configuration: ${error.message}</div>`;
}
if (typeof router !== 'undefined' && router.hideLoadingBar) {
router.hideLoadingBar();
}
}
}
async loadConfigComponentManual(categoryId) {
const configSection = document.getElementById('config-section');
// This method is no longer needed since we use ConfigManager for all categories
// The individual config classes have been removed
//console.log(`ConfigRouter: loadConfigComponentManual called for ${categoryId} - delegating to ConfigManager`);
// Use ConfigManager for all categories now
if (window.configManager) {
await window.configManager.renderConfig(categoryId);
} else {
configSection.innerHTML = '<div class="error">ConfigManager not available</div>';
}
// Hide loading bar
if (typeof router !== 'undefined' && router.hideLoadingBar) {
router.hideLoadingBar();
}
}
async loadScript(src) {
// Check if script is already loaded
const scriptId = src.replace(/[^a-zA-Z0-9]/g, '_');
if (document.getElementById(scriptId)) {
//console.log(`Script ${src} already loaded, skipping`);
return;
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.id = scriptId;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
}
// Export for global access
window.ConfigRouter = ConfigRouter;