librelad 07daa7555a fix(webui): load mobile-menu.js; prune orphaned task queue entries
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>
2026-08-12 22:48:55 +01:00

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() {
}