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

129 lines
4.8 KiB
JavaScript
Executable File

// Dashboard functionality
// loadSystemInfo() and updateDiskChart() live in data-loader.js — that version
// uses waitForDashboardElements() so it doesn't fire before the dashboard HTML
// is in the DOM. Defining them here too overrode the safer version and produced
// the spurious "Disk chart elements not found" errors when called from non-dashboard pages.
// Load installed apps and render icon grid on dashboard
async function loadInstalledApps() {
if (!window.apps || window.apps.length === 0) {
try {
const response = await fetch('/data/apps/generated/apps.json', { cache: 'no-store' });
const data = await response.json();
window.apps = data.apps || [];
} catch (e) {
return;
}
}
renderInstalledApps();
}
function renderInstalledApps() {
const section = document.getElementById('frontpage-apps-section');
const container = document.getElementById('frontpage-apps-container');
if (!section || !container) return;
const installed = (window.apps || []).filter(a => a.installed);
if (installed.length === 0) return;
container.innerHTML = installed.map(app => createInstalledAppCard(app)).join('');
section.style.display = '';
populateDashboardServiceButtons(installed);
}
function createInstalledAppCard(app) {
const appName = app.command.split(' ').pop();
let icon = app.icon || '/core/icons/apps/default.svg';
if (!icon.startsWith('/')) icon = '/' + icon;
const shortName = app.name.split(' - ')[0].trim();
return `
<div class="frontpage-app-tile" onclick="window.location.href='/app/${appName}'">
<div class="frontpage-app-icon-wrap">
<img src="${icon}" alt="${shortName}" onerror="this.src='/core/icons/apps/default.svg'">
<div class="frontpage-app-overlay" id="frontpage-overlay-${appName}" onclick="event.stopPropagation()"></div>
</div>
<span class="frontpage-app-name">${shortName}</span>
</div>
`;
}
async function populateDashboardServiceButtons(installedApps) {
let services = [];
if (window.serviceButtons) {
if (window.serviceButtons.services.length === 0) await window.serviceButtons.loadServices();
services = window.serviceButtons.services;
} else {
try {
const res = await fetch('/data/apps/generated/apps-services.json', { cache: 'no-store' });
const data = await res.json();
services = data.services || [];
} catch (e) {
return;
}
}
const proto = s => ['http', 'https'].includes((s.protocol || '').toLowerCase()) ? s.protocol.toLowerCase() : 'http';
installedApps.forEach(app => {
const appName = app.command.split(' ').pop();
const shortName = app.name.split(' - ')[0].trim();
const overlay = document.getElementById(`frontpage-overlay-${appName}`);
if (!overlay) return;
const appServices = services.filter(s => s.app === appName && s.buttonEnabled === true);
// Multi-button render via the shared expandServiceLinks() helper.
const serviceButtons = appServices.flatMap(s =>
window.expandServiceLinks(s).map(({ url, label }) => `
<a href="${url}" target="_blank" rel="noopener noreferrer" class="frontpage-overlay-btn" onclick="event.stopPropagation()">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
${label}
</a>
`)
).filter(Boolean).join('');
overlay.innerHTML = serviceButtons + `<button class="frontpage-app-manage-btn" onclick="event.stopPropagation(); navigateToApp('${appName}')">${shortName}</button>`;
});
}
// Setup event listeners
function setupEventListeners() {
setupMobileMenu();
loadInstalledApps();
}
// Navigate to app page using SPA router
function navigateToApp(appName) {
// Use proper SPA navigation to the app page
if (window.librePortalSPA && typeof window.librePortalSPA.navigate === 'function') {
window.librePortalSPA.navigate(`/app/${appName}`);
} else if (window.navigateToRoute && typeof window.navigateToRoute === 'function') {
window.navigateToRoute(`app/${appName}`);
} else {
// Fallback to direct navigation
window.location.href = `/app/${appName}`;
}
}
// Filter apps by search term (removed - not used in dashboard)
function filterApps(searchTerm) {
//console.log('Filter apps functionality removed from dashboard');
}
// Filter apps by category (removed - not used in dashboard)
function filterAppsByCategory(category) {
//console.log('Filter apps by category functionality removed from dashboard');
}
// Populate category filter (removed - not used in dashboard)
function populateCategoryFilter() {
//console.log('Category filter population removed from dashboard');
}