Both found in a user's console log. 1. ReferenceError: setupMobileMenu is not defined (dashboard.js:98) core/topbar/js/mobile-menu.js defines that global, and index.html never loaded it. dashboard.js called it unguarded as the FIRST line of setupEventListeners, so dashboard init threw every page load and took loadInstalledApps() with it — and the burger menu was dead on mobile. system-loader already guarded its own call with a typeof check, which is why this survived unnoticed. Loads the script (before dashboard.js) and guards the call, so optional nav chrome can never take down the page below it again. 2. Endless 404s on /api/tasks/<id> for tasks that no longer exist queue.json is append-only from the enqueue side and nothing ever pruned it, so any task file removed afterwards left an id the WebUI re-fetched forever, one 404 per poll per orphan. Adds cleanupOrphanQueueEntries to the idle housekeeping pass: entries with no task file are dropped and logged. Self-heals existing strays. (Provoked by my own clean-up of two test tasks earlier in this session, but the gap is real and predates it.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
5.0 KiB
JavaScript
Executable File
133 lines
5.0 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() {
|
|
// Guarded like system-loader's mobile-menu component: this is a nav nicety,
|
|
// and an unguarded call meant a missing/reordered script took the dashboard's
|
|
// app list down with it. Never let optional chrome break the page below it.
|
|
if (typeof setupMobileMenu === 'function') {
|
|
setupMobileMenu();
|
|
} else {
|
|
console.warn('setupMobileMenu not available — mobile drawer disabled');
|
|
}
|
|
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) {
|
|
}
|
|
|
|
// Filter apps by category (removed - not used in dashboard)
|
|
function filterAppsByCategory(category) {
|
|
}
|
|
|
|
// Populate category filter (removed - not used in dashboard)
|
|
function populateCategoryFilter() {
|
|
}
|