Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
9eb5f7f73a Merge claude/1 2026-05-30 12:42:35 +01:00
librelad
2ef4cc00e1 refactor(webui): granular sub-system folders per component
De-clutter each component into sub-system folders (apps: core/ port-manager/
services/ tools/ routing/; admin: config/ overview/ system/ ssh/ peers/) with
the standard js/ css/ html/ icons/ layout inside; single-page components
(backup/dashboard/tasks/updater) get js/ css/ html/. Single-feature icon sets
moved into their sub-system (vpn -> apps/core/icons, config/cpu/os ->
admin/{config,system}/icons); shared app + category icons stay in core/icons.
feature.json + index.js stay at each component root (the scanned descriptor +
entry). Every controller/CSS/fragment/icon path reference rewritten; verified
no stale refs, all JS valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-30 12:42:35 +01:00
94 changed files with 65 additions and 65 deletions

View File

@ -44,8 +44,8 @@ if (typeof window.ConfigManager === 'undefined') {
try { this.sidebar.populateSidebar(); } catch (e) {}
// charts.js is the chart-rendering helper admin-overview pulls in.
await Promise.all([
lazyLoad('/components/admin/admin-overview.js'),
lazyLoad('/components/admin/charts.js')
lazyLoad('/components/admin/overview/js/admin-overview.js'),
lazyLoad('/components/admin/system/js/charts.js')
]);
if (typeof AdminOverview !== 'undefined') {
window.adminOverview = new AdminOverview('config-section');
@ -60,7 +60,7 @@ if (typeof window.ConfigManager === 'undefined') {
// a config category — render its own controller into the main pane.
if (category === 'ssh-access') {
try { this.sidebar.populateSidebar(); } catch (e) {}
await lazyLoad('/components/admin/ssh-page.js');
await lazyLoad('/components/admin/ssh/js/ssh-page.js');
if (typeof SshPage !== 'undefined') {
window.sshPage = new SshPage('config-section');
await window.sshPage.init();
@ -76,9 +76,9 @@ if (typeof window.ConfigManager === 'undefined') {
// we inject its content template, then init PeersPage.
if (category === 'peers') {
try { this.sidebar.populateSidebar(); } catch (e) {}
await lazyLoad('/components/admin/peers-page.js');
await lazyLoad('/components/admin/peers/js/peers-page.js');
try {
const html = await fetch('/components/admin/peers-content.html').then(r => r.text());
const html = await fetch('/components/admin/peers/html/peers-content.html').then(r => r.text());
configSection.innerHTML = html;
} catch (e) {
configSection.innerHTML = '<div class="error">Peers page template failed to load.</div>';
@ -100,10 +100,10 @@ if (typeof window.ConfigManager === 'undefined') {
if (category === 'system') {
try { this.sidebar.populateSidebar(); } catch (e) {}
await Promise.all([
lazyLoad('/components/admin/charts.js'),
lazyLoad('/components/admin/admin-system.js'),
lazyLoad('/components/admin/system-metric-page.js'),
lazyLoad('/components/admin/system-storage-page.js')
lazyLoad('/components/admin/system/js/charts.js'),
lazyLoad('/components/admin/system/js/admin-system.js'),
lazyLoad('/components/admin/system/js/system-metric-page.js'),
lazyLoad('/components/admin/system/js/system-storage-page.js')
]);
if (typeof AdminSystem !== 'undefined') {
window.adminSystem = new AdminSystem('config-section');
@ -216,7 +216,7 @@ if (typeof window.ConfigManager === 'undefined') {
var catIcon = catMeta.icon || category;
var headerHTML =
'<div class="page-header config-page-header">' +
'<img class="page-header-icon" src="/core/icons/config/' + catIcon + '.svg" alt="" onerror="this.style.display=\'none\'">' +
'<img class="page-header-icon" src="/components/admin/config/icons/config/' + catIcon + '.svg" alt="" onerror="this.style.display=\'none\'">' +
'<div class="page-header-title">' +
'<div class="admin-breadcrumb">Config</div>' +
'<h1>' + catTitle + '</h1>' +

View File

@ -88,7 +88,7 @@ class ConfigSidebar {
// Use correct icon from our new structure
const iconName = category.icon || category.id;
const iconPath = '/core/icons/config/' + iconName + '.svg';
const iconPath = '/components/admin/config/icons/config/' + iconName + '.svg';
categoryItem.innerHTML = '<img src="' + iconPath + '" alt="' + category.title + '" style="width: 20px; height: 20px; margin-right: 8px;"/> ' + category.title;

View File

@ -16,7 +16,7 @@ LP.features.register({
async mount(ctx) {
window.configCategory = ctx.services.router.adminCategoryFromPath(window.location.pathname);
const html = await ctx.loadFragment('/components/admin/config-content.html');
const html = await ctx.loadFragment('/components/admin/config/html/config-content.html');
ctx.setContent(html, 'Admin');
if (window.configManager) {

View File

Before

Width:  |  Height:  |  Size: 583 B

After

Width:  |  Height:  |  Size: 583 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 780 B

After

Width:  |  Height:  |  Size: 780 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 911 B

After

Width:  |  Height:  |  Size: 911 B

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 963 B

View File

@ -401,14 +401,14 @@ class AdminSystem {
return `<div class="sys-stat"><span class="sys-stat-label">${this.escape(label)}</span><strong class="sys-stat-value">${this.escape(value)}</strong></div>`;
}
// OS stat with a distro icon (bundled under /core/icons/os/, generic Linux glyph
// OS stat with a distro icon (bundled under /components/admin/system/icons/os/, generic Linux glyph
// for anything we don't have a logo for).
_osStat(os) {
const slug = String(os || '').toLowerCase().replace(/[^a-z0-9]/g, '') || 'linux';
return `<div class="sys-stat">
<span class="sys-stat-label">OS</span>
<strong class="sys-stat-value sys-id-value">
<span class="sys-id-icon"><img src="/core/icons/os/${slug}.svg" alt="" onerror="this.onerror=null;this.src='/core/icons/os/linux.svg'"></span>
<span class="sys-id-icon"><img src="/components/admin/system/icons/os/${slug}.svg" alt="" onerror="this.onerror=null;this.src='/components/admin/system/icons/os/linux.svg'"></span>
${this.escape(os || '—')}
</strong>
</div>`;
@ -430,7 +430,7 @@ class AdminSystem {
_cpuStat(cpu) {
const raw = String(cpu || '');
const vendor = /amd/i.test(raw) ? 'amd' : /intel/i.test(raw) ? 'intel' : null;
const icon = vendor ? `<span class="sys-id-icon"><img src="/core/icons/cpu/${vendor}.svg" alt="${vendor}"></span>` : '';
const icon = vendor ? `<span class="sys-id-icon"><img src="/components/admin/system/icons/cpu/${vendor}.svg" alt="${vendor}"></span>` : '';
return `<div class="sys-stat">
<span class="sys-stat-label">CPU</span>
<strong class="sys-stat-value sys-id-value">${icon}${this.escape(this._cleanCpu(raw))}</strong>

View File

@ -42,7 +42,7 @@ LP.features.register({
}
}
const html = await ctx.loadFragment('/components/apps/apps-unified-layout.html');
const html = await ctx.loadFragment('/components/apps/core/html/apps-unified-layout.html');
ctx.setContent(html, appName);
if (!window.appTabbedManager) {

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 952 B

After

Width:  |  Height:  |  Size: 952 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -2408,7 +2408,7 @@ class AppsManager {
const existing = document.getElementById('mullvad-generate-modal');
if (existing) existing.remove();
const mullvadIcon = (window.gluetunProviderIcons && window.gluetunProviderIcons.mullvad) || '/core/icons/vpn/mullvad.svg';
const mullvadIcon = (window.gluetunProviderIcons && window.gluetunProviderIcons.mullvad) || '/components/apps/core/icons/vpn/mullvad.svg';
const bodyHtml = `
<p class="eo-modal-section-text">
Enter your 16-digit Mullvad account number. A new WireGuard key will be generated locally
@ -3354,7 +3354,7 @@ class AppsManager {
{ name: 'TaskCommands', src: '/core/lib/task-commands.js' },
{ name: 'TaskActions', src: '/core/lib/task-actions.js' },
{ name: 'TaskRouter', src: '/core/lib/task-router.js' },
{ name: 'TasksManager', src: '/components/tasks/tasks-manager.js' }
{ name: 'TasksManager', src: '/components/tasks/js/tasks-manager.js' }
];
for (const script of scripts) {

View File

@ -28,7 +28,7 @@ LP.features.register({
// Load the unified layout only if it isn't already present — preserves grid
// state when moving between categories / back from app-detail (legacy behaviour).
if (!document.querySelector('.apps-layout')) {
const html = await ctx.loadFragment('/components/apps/apps-unified-layout.html');
const html = await ctx.loadFragment('/components/apps/core/html/apps-unified-layout.html');
ctx.setContent(html, 'Applications');
}

View File

@ -14,14 +14,14 @@ LP.features.register({
routes: ['/backup', '/backup*'],
// Controllers the feature needs; lazy-loaded on first mount (idempotent).
scripts: [
'/components/backup/backup-schema.js',
'/components/backup/backup-page.js',
'/components/backup/js/backup-schema.js',
'/components/backup/js/backup-page.js',
'/core/lib/backup-app-card.js',
],
async mount(ctx) {
await ctx.loadScripts(this.scripts);
const html = await ctx.loadFragment('/components/backup/backup-content.html');
const html = await ctx.loadFragment('/components/backup/html/backup-content.html');
ctx.setContent(html, 'Backups');
if (typeof BackupPage === 'undefined') {
throw new Error('BackupPage controller failed to load');

View File

@ -13,7 +13,7 @@ LP.features.register({
routes: ['/', '/dashboard'],
async mount(ctx) {
const html = await ctx.loadFragment('/components/dashboard/dashboard-content.html');
const html = await ctx.loadFragment('/components/dashboard/html/dashboard-content.html');
ctx.setContent(html, 'Dashboard');
// Render the installed-apps icon grid (handleDashboard's only post-render call).

View File

@ -11,7 +11,7 @@ LP.features.register({
routes: ['/tasks', '/tasks*'],
async mount(ctx) {
const html = await ctx.loadFragment('/components/tasks/tasks-content.html');
const html = await ctx.loadFragment('/components/tasks/html/tasks-content.html');
ctx.setContent(html, 'Tasks');
if (window.tasksManager) {

View File

@ -9,11 +9,11 @@
LP.features.register({
id: 'updater',
routes: ['/updater', '/updater*'],
scripts: ['/components/updater/updater-page.js'],
scripts: ['/components/updater/js/updater-page.js'],
async mount(ctx) {
await ctx.loadScripts(this.scripts);
const html = await ctx.loadFragment('/components/updater/updater-content.html');
const html = await ctx.loadFragment('/components/updater/html/updater-content.html');
ctx.setContent(html, 'Updates');
if (typeof UpdaterPage === 'undefined') {
throw new Error('UpdaterPage controller failed to load');

View File

@ -41,16 +41,16 @@ class SystemLoader {
scripts: [
'/core/lib/config-options.js',
'/core/lib/config-shared.js',
'/components/admin/config-validator.js',
'/components/admin/toggle-manager.js',
'/components/admin/config-core.js',
'/components/admin/domain-manager.js',
'/components/admin/ip-whitelist-manager.js',
'/components/admin/config-renderer.js',
'/components/admin/config-sidebar.js',
'/components/admin/config-form.js',
'/components/admin/config-utils.js',
'/components/admin/config-manager.js'
'/components/admin/config/js/config-validator.js',
'/components/admin/config/js/toggle-manager.js',
'/components/admin/config/js/config-core.js',
'/components/admin/config/js/domain-manager.js',
'/components/admin/config/js/ip-whitelist-manager.js',
'/components/admin/config/js/config-renderer.js',
'/components/admin/config/js/config-sidebar.js',
'/components/admin/config/js/config-form.js',
'/components/admin/config/js/config-utils.js',
'/components/admin/config/js/config-manager.js'
]
});
@ -174,7 +174,7 @@ class SystemLoader {
'/core/lib/task-global-functions.js',
'/core/lib/task-manager.js',
'/core/lib/task-parameter-preserve.js',
'/components/tasks/tasks-manager.js'
'/components/tasks/js/tasks-manager.js'
]
});
@ -204,13 +204,13 @@ class SystemLoader {
global: 'appsManager',
dependencies: ['data'],
scripts: [
'/components/apps/port-manager.js',
'/components/apps/port-manager/js/port-manager.js',
'/core/lib/task-manager.js', // Add TaskManager for backup functionality
'/core/lib/backup-app-card.js',
'/components/apps/services-manager.js',
'/components/apps/tools-manager.js',
'/components/apps/routing-manager.js',
'/components/apps/apps-manager.js'
'/components/apps/services/js/services-manager.js',
'/components/apps/tools/js/tools-manager.js',
'/components/apps/routing/js/routing-manager.js',
'/components/apps/core/js/apps-manager.js'
]
});
@ -232,7 +232,7 @@ class SystemLoader {
},
global: 'appTabbedManager',
dependencies: [],
script: '/components/apps/app-tabbed-manager.js'
script: '/components/apps/core/js/app-tabbed-manager.js'
});
// console.log('TEST: Components added. Total components:', this.components.size);

View File

@ -341,7 +341,7 @@ class LibrePortalSPAClean {
try {
// console.log('📄 SPA: Fetching dashboard content...');
const html = await this.fetchContent('/components/dashboard/dashboard-content.html');
const html = await this.fetchContent('/components/dashboard/html/dashboard-content.html');
// console.log('📄 SPA: Dashboard content fetched, loading...');
this.loadContent(html, 'Dashboard');
// console.log('📄 SPA: Dashboard content loaded');
@ -362,10 +362,10 @@ class LibrePortalSPAClean {
// backup-page.js + backup-app-card.js are loaded on first navigation.
// loadScript is idempotent — subsequent /backup navigations are no-ops.
await Promise.all([
this.loadScript('/components/backup/backup-page.js'),
this.loadScript('/components/backup/js/backup-page.js'),
this.loadScript('/core/lib/backup-app-card.js')
]);
const html = await this.fetchContent('/components/backup/backup-content.html');
const html = await this.fetchContent('/components/backup/html/backup-content.html');
this.loadContent(html, 'Backups');
if (typeof BackupPage !== 'undefined') {
window.backupPage = new BackupPage();
@ -409,7 +409,7 @@ class LibrePortalSPAClean {
// Ensure unified layout is loaded (like the old SPA)
if (!document.querySelector('.apps-layout')) {
//console.log('📄 Loading apps layout HTML...');
const html = await this.fetchContent('/components/apps/apps-unified-layout.html');
const html = await this.fetchContent('/components/apps/core/html/apps-unified-layout.html');
this.loadContent(html, 'Applications');
} else {
//console.log('📄 Apps layout already exists, skipping HTML load');
@ -477,7 +477,7 @@ class LibrePortalSPAClean {
}
try {
const html = await this.fetchContent('/components/apps/apps-unified-layout.html');
const html = await this.fetchContent('/components/apps/core/html/apps-unified-layout.html');
this.loadContent(html, appName); // Will be updated after app data loads
// AppTabbedManager should already be initialized by SystemLoader
@ -502,7 +502,7 @@ class LibrePortalSPAClean {
window.configCategory = window.adminCategoryFromPath(window.location.pathname);
try {
const html = await this.fetchContent('/components/admin/config-content.html');
const html = await this.fetchContent('/components/admin/config/html/config-content.html');
this.loadContent(html, 'Admin');
if (window.configManager) {
@ -535,7 +535,7 @@ class LibrePortalSPAClean {
//console.log('📋 Loading tasks...');
try {
const html = await this.fetchContent('/components/tasks/tasks-content.html');
const html = await this.fetchContent('/components/tasks/html/tasks-content.html');
this.loadContent(html, 'Tasks');
// Tasks manager should already be initialized by SystemLoader

View File

@ -16,27 +16,27 @@
<link rel="stylesheet" href="/core/css/loading-screen.css">
<link rel="stylesheet" href="/core/css/setup-wizard.css">
<link rel="stylesheet" href="/core/css/style.css">
<link rel="stylesheet" href="/components/admin/ip-whitelist.css">
<link rel="stylesheet" href="/components/apps/port-manager.css">
<link rel="stylesheet" href="/components/backup/backup.css">
<link rel="stylesheet" href="/components/admin/ssh.css">
<link rel="stylesheet" href="/components/admin/admin.css">
<link rel="stylesheet" href="/components/apps/services.css">
<link rel="stylesheet" href="/components/admin/config/css/ip-whitelist.css">
<link rel="stylesheet" href="/components/apps/port-manager/css/port-manager.css">
<link rel="stylesheet" href="/components/backup/css/backup.css">
<link rel="stylesheet" href="/components/admin/ssh/css/ssh.css">
<link rel="stylesheet" href="/components/admin/css/admin.css">
<link rel="stylesheet" href="/components/apps/services/css/services.css">
<link rel="stylesheet" href="/core/css/modal.css">
<link rel="stylesheet" href="/components/apps/tools.css">
<link rel="stylesheet" href="/components/apps/routing.css">
<link rel="stylesheet" href="/components/apps/tools/css/tools.css">
<link rel="stylesheet" href="/components/apps/routing/css/routing.css">
<link rel="stylesheet" href="/core/css/login.css">
<link rel="stylesheet" href="/core/css/aurora-background.css">
<link rel="stylesheet" href="/core/css/topbar.css">
<link rel="stylesheet" href="/core/css/sidebar.css">
<link rel="stylesheet" href="/components/apps/apps-layout.css">
<link rel="stylesheet" href="/components/apps/apps.css">
<link rel="stylesheet" href="/components/apps/core/css/apps-layout.css">
<link rel="stylesheet" href="/components/apps/core/css/apps.css">
<link rel="stylesheet" href="/core/css/forms.css">
<link rel="stylesheet" href="/core/css/config.css">
<link rel="stylesheet" href="/components/apps/service-buttons.css">
<link rel="stylesheet" href="/components/dashboard/dashboard.css">
<link rel="stylesheet" href="/components/tasks/tasks.css">
<link rel="stylesheet" href="/components/updater/updater.css">
<link rel="stylesheet" href="/components/apps/core/css/service-buttons.css">
<link rel="stylesheet" href="/components/dashboard/css/dashboard.css">
<link rel="stylesheet" href="/components/tasks/css/tasks.css">
<link rel="stylesheet" href="/components/updater/css/updater.css">
<link rel="stylesheet" href="/core/css/update-notifier.css">
<script>
// Inline data-theme bootstrap — runs before any rendering so the right
@ -98,7 +98,7 @@
<script src="/core/lib/dismissible.js"></script>
<script src="/core/ui/eo-modal.js"></script>
<script src="/core/lib/task-refresh-coordinator.js"></script>
<script src="/components/dashboard/dashboard.js"></script>
<script src="/components/dashboard/js/dashboard.js"></script>
<script src="/core/boot/system-loader.js"></script>
<script src="/core/boot/loading-ui.js"></script>
<script src="/core/boot/setup-detector.js"></script>