fix(webui): make all icon and data asset URLs absolute under path routing
Same class of bug as the topbar partial: icon and data-file references were relative (icons/apps/x.svg, data/apps/...), so on deep path routes (/app/<name>, /admin/config/x) the browser resolved them against the route dir and the SPA catch-all served index.html with HTTP 200 instead of 404 — broken images and silently-wrong JSON. Make every reference absolute (anchored on the quote/backtick so already-absolute /icons paths are untouched): - JS: all icons/ and data/ literals + templates across components/utils/system - html/topbar.html: logo <img> - generators: webui_config.sh and webui_create_app_categories.sh now emit /icons/... into apps.json / apps-categories.json (regenerated on install) - updated the two icon-path comments to match Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
45d2f5f5c7
commit
152d9c5d28
@ -9,7 +9,7 @@
|
||||
</button>
|
||||
<div class="topbar-left">
|
||||
<div class="libreportal-logo">
|
||||
<img src="icons/libreportal.svg" alt="LibrePortal" width="32" height="32">
|
||||
<img src="/icons/libreportal.svg" alt="LibrePortal" width="32" height="32">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-drawer" id="mobile-drawer">
|
||||
|
||||
@ -652,7 +652,7 @@ class AppsManager {
|
||||
if (onHiddenTab) {
|
||||
window.appTabbedManager.switchTab('config');
|
||||
}
|
||||
let icon = app.icon || 'icons/apps/default.svg';
|
||||
let icon = app.icon || '/icons/apps/default.svg';
|
||||
|
||||
// Ensure absolute path from root
|
||||
if (!icon.startsWith('/')) {
|
||||
@ -672,7 +672,7 @@ class AppsManager {
|
||||
const headerHTML = `
|
||||
<div class="app-info">
|
||||
<div class="app-card-icon" style="width: 80px; height: 80px; min-width: 80px;">
|
||||
<img src="${icon}" alt="${shortName}" onerror="this.onerror=null; this.src='icons/apps/default.svg'" style="width: 100%; height: 100%; object-fit: contain;"/>
|
||||
<img src="${icon}" alt="${shortName}" onerror="this.onerror=null; this.src='/icons/apps/default.svg'" style="width: 100%; height: 100%; object-fit: contain;"/>
|
||||
</div>
|
||||
<div class="app-details">
|
||||
<h2>${shortName}</h2>
|
||||
@ -1701,7 +1701,7 @@ class AppsManager {
|
||||
card.dataset.search = searchHaystack;
|
||||
|
||||
const appName = app.command.split(' ').pop();
|
||||
let icon = app.icon || 'icons/apps/default.svg';
|
||||
let icon = app.icon || '/icons/apps/default.svg';
|
||||
|
||||
// Ensure absolute path from root
|
||||
if (!icon.startsWith('/')) {
|
||||
@ -2476,7 +2476,7 @@ class AppsManager {
|
||||
async getFieldMappings() {
|
||||
try {
|
||||
// Load from apps folder (static file)
|
||||
const response = await fetch('data/apps/apps-field-mappings.json');
|
||||
const response = await fetch('/data/apps/apps-field-mappings.json');
|
||||
const data = await response.json();
|
||||
//// // console.log('✅ Loaded field mappings from apps folder');
|
||||
return data.fields || data;
|
||||
|
||||
@ -577,7 +577,7 @@ class BackupPage {
|
||||
const command = a.command || '';
|
||||
return command.endsWith(` ${slug}`) || a.name?.toLowerCase() === slug.toLowerCase();
|
||||
});
|
||||
let icon = match?.icon || 'icons/apps/default.svg';
|
||||
let icon = match?.icon || '/icons/apps/default.svg';
|
||||
if (!icon.startsWith('/')) icon = '/' + icon;
|
||||
const displayName = (typeof window.getAppDisplayName === 'function')
|
||||
? window.getAppDisplayName(slug)
|
||||
|
||||
@ -23,7 +23,7 @@ window.ConfigValidator = function() {
|
||||
|
||||
// Check if unified config file exists (file existence check only)
|
||||
const configFiles = [
|
||||
{ name: 'Unified System Config', path: 'data/config/generated/configs.json' }
|
||||
{ name: 'Unified System Config', path: '/data/config/generated/configs.json' }
|
||||
];
|
||||
|
||||
for (const config of configFiles) {
|
||||
|
||||
@ -34,7 +34,7 @@ function renderInstalledApps() {
|
||||
|
||||
function createInstalledAppCard(app) {
|
||||
const appName = app.command.split(' ').pop();
|
||||
let icon = app.icon || 'icons/apps/default.svg';
|
||||
let icon = app.icon || '/icons/apps/default.svg';
|
||||
if (!icon.startsWith('/')) icon = '/' + icon;
|
||||
const shortName = app.name.split(' - ')[0].trim();
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ class NotificationSystem {
|
||||
if (appIcon) {
|
||||
content += `
|
||||
<div class="notification-app-icon">
|
||||
<img src="${appIcon}" alt="${appName}" onerror="this.onerror=null; this.src='icons/apps/default.svg'">
|
||||
<img src="${appIcon}" alt="${appName}" onerror="this.onerror=null; this.src='/icons/apps/default.svg'">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -290,7 +290,7 @@ async configUpdate(changes) {
|
||||
} catch (error) {
|
||||
console.warn('Could not get app data:', error);
|
||||
}
|
||||
const appIcon = appData?.icon || `icons/apps/${task.app}.svg`;
|
||||
const appIcon = appData?.icon || `/icons/apps/${task.app}.svg`;
|
||||
|
||||
let taskUrl;
|
||||
const currentUrl = window.location.href;
|
||||
@ -356,7 +356,7 @@ async configUpdate(changes) {
|
||||
} catch (error) {
|
||||
console.warn('Could not get app data:', error);
|
||||
}
|
||||
const appIcon = appData?.icon || `icons/apps/${task.app}.svg`;
|
||||
const appIcon = appData?.icon || `/icons/apps/${task.app}.svg`;
|
||||
|
||||
// Smart URL generation - always include app name
|
||||
let taskUrl;
|
||||
|
||||
@ -331,7 +331,7 @@ class TaskCommands {
|
||||
// Fallback: create minimal app data
|
||||
return {
|
||||
name: appName,
|
||||
icon: `icons/apps/${appName}.svg`,
|
||||
icon: `/icons/apps/${appName}.svg`,
|
||||
command: `libreportal app install ${appName}`
|
||||
};
|
||||
}
|
||||
|
||||
@ -1035,7 +1035,7 @@ class TasksManager {
|
||||
}
|
||||
|
||||
// Default icon path
|
||||
return `icons/apps/${task.app}.svg`;
|
||||
return `/icons/apps/${task.app}.svg`;
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
@ -1343,7 +1343,7 @@ class TasksManager {
|
||||
'info',
|
||||
appName,
|
||||
taskUrl,
|
||||
`icons/apps/${appName}.svg`,
|
||||
`/icons/apps/${appName}.svg`,
|
||||
customIcon
|
||||
);
|
||||
}
|
||||
@ -1458,7 +1458,7 @@ class TasksManager {
|
||||
const url = (onAppPage && appName)
|
||||
? `/app/${appName}?tab=tasks&task=${taskId}`
|
||||
: `/tasks/all?task=${taskId}`;
|
||||
const icon = appName ? `icons/apps/${appName}.svg` : null;
|
||||
const icon = appName ? `/icons/apps/${appName}.svg` : null;
|
||||
|
||||
// Match the per-action emoji used in the task list rows (see
|
||||
// `getTaskTypeIcon`). Passed as the 6th `customIcon` arg so the
|
||||
|
||||
@ -327,7 +327,7 @@ class SetupWizard {
|
||||
const app = manifest.get(slug) || {};
|
||||
const name = app.title || app.name || fallback.name;
|
||||
const desc = app.description || fallback.description;
|
||||
const icon = app.icon || `icons/apps/${slug}.svg`;
|
||||
const icon = app.icon || `/icons/apps/${slug}.svg`;
|
||||
// Sub-option lives OUTSIDE the parent label (nested <label> behaviour
|
||||
// is messy — clicks would toggle both checkboxes). Both labels are
|
||||
// siblings inside a wrapper div; CSS connects them visually.
|
||||
@ -342,7 +342,7 @@ class SetupWizard {
|
||||
<input type="checkbox" data-app="${slug}" ${defaultChecked ? 'checked' : ''}>
|
||||
<div class="setup-app-icon-wrap">
|
||||
<img src="${icon}" alt="${name}" class="setup-app-icon"
|
||||
onerror="this.onerror=null; this.src='icons/apps/default.svg'">
|
||||
onerror="this.onerror=null; this.src='/icons/apps/default.svg'">
|
||||
</div>
|
||||
<div class="setup-app-info">
|
||||
<div class="setup-app-name">${name}</div>
|
||||
|
||||
@ -118,7 +118,7 @@ let categories = [];
|
||||
|
||||
function getCategoryIcon(categoryId) {
|
||||
const category = categories.find(cat => cat.id === categoryId);
|
||||
return category ? category.icon : 'icons/categories/misc.svg';
|
||||
return category ? category.icon : '/icons/categories/misc.svg';
|
||||
}
|
||||
|
||||
function getCategoryName(categoryId) {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
function getAppIcon(appName, appIconUrl) {
|
||||
const cleanAppName = appName.replace('install_', '').replace(' ', '');
|
||||
// Use icon URL from JSON if available, otherwise fall back to default
|
||||
return appIconUrl || `icons/apps/${cleanAppName}.svg`;
|
||||
return appIconUrl || `/icons/apps/${cleanAppName}.svg`;
|
||||
}
|
||||
|
||||
function getAppStatus(installed) {
|
||||
|
||||
@ -212,7 +212,7 @@ EOF
|
||||
fields+=(" \"backup_live_capable\": $backup_live_capable")
|
||||
[[ -n "$url" ]] && fields+=(" \"url\": \"$url\"")
|
||||
[[ -n "$description" ]] && fields+=(" \"description\": \"$description\"")
|
||||
[[ -n "$icon_file" ]] && fields+=(" \"icon\": \"icons/apps/$icon_file\"")
|
||||
[[ -n "$icon_file" ]] && fields+=(" \"icon\": \"/icons/apps/$icon_file\"")
|
||||
[[ -n "$services_json" ]] && fields+=(" \"services\": [$services_json]")
|
||||
if [[ -n "$config_vars" ]]; then
|
||||
fields+=(" \"config\": {"$'\n'"${config_vars%,}"$'\n'" }")
|
||||
|
||||
@ -26,67 +26,67 @@ webuiCreateAppsCategories() {
|
||||
{
|
||||
"id": "recommended",
|
||||
"name": "Recommended",
|
||||
"icon": "icons/categories/recommended.svg",
|
||||
"icon": "/icons/categories/recommended.svg",
|
||||
"description": "Hand-picked apps to round out your install"
|
||||
},
|
||||
{
|
||||
"id": "communication",
|
||||
"name": "Communication",
|
||||
"icon": "icons/categories/communication.svg",
|
||||
"icon": "/icons/categories/communication.svg",
|
||||
"description": "Communication and collaboration tools"
|
||||
},
|
||||
{
|
||||
"id": "development",
|
||||
"name": "Development",
|
||||
"icon": "icons/categories/development.svg",
|
||||
"icon": "/icons/categories/development.svg",
|
||||
"description": "Development tools and version control"
|
||||
},
|
||||
{
|
||||
"id": "knowledge",
|
||||
"name": "Knowledge",
|
||||
"icon": "icons/categories/knowledge.svg",
|
||||
"icon": "/icons/categories/knowledge.svg",
|
||||
"description": "Knowledge management and wikis"
|
||||
},
|
||||
{
|
||||
"id": "media",
|
||||
"name": "Media",
|
||||
"icon": "icons/categories/media.svg",
|
||||
"icon": "/icons/categories/media.svg",
|
||||
"description": "Media streaming and entertainment"
|
||||
},
|
||||
{
|
||||
"id": "monitoring",
|
||||
"name": "Monitoring",
|
||||
"icon": "icons/categories/monitoring.svg",
|
||||
"icon": "/icons/categories/monitoring.svg",
|
||||
"description": "Metrics, dashboards and observability"
|
||||
},
|
||||
{
|
||||
"id": "networking",
|
||||
"name": "Networking",
|
||||
"icon": "icons/categories/networking.svg",
|
||||
"icon": "/icons/categories/networking.svg",
|
||||
"description": "Network tools and services"
|
||||
},
|
||||
{
|
||||
"id": "productivity",
|
||||
"name": "Productivity",
|
||||
"icon": "icons/categories/productivity.svg",
|
||||
"icon": "/icons/categories/productivity.svg",
|
||||
"description": "Productivity and business tools"
|
||||
},
|
||||
{
|
||||
"id": "security",
|
||||
"name": "Security",
|
||||
"icon": "icons/categories/security.svg",
|
||||
"icon": "/icons/categories/security.svg",
|
||||
"description": "Security and privacy tools"
|
||||
},
|
||||
{
|
||||
"id": "storage",
|
||||
"name": "Storage",
|
||||
"icon": "icons/categories/storage.svg",
|
||||
"icon": "/icons/categories/storage.svg",
|
||||
"description": "File storage and sharing solutions"
|
||||
},
|
||||
{
|
||||
"id": "system",
|
||||
"name": "System",
|
||||
"icon": "icons/categories/system.svg",
|
||||
"icon": "/icons/categories/system.svg",
|
||||
"description": "LibrePortal itself and platform-level services"
|
||||
}
|
||||
]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
# Sync app icons from container template dirs to the LibrePortal frontend.
|
||||
#
|
||||
# apps.json references icons/apps/<app>.<ext> unconditionally (see
|
||||
# apps.json references /icons/apps/<app>.<ext> unconditionally (see
|
||||
# webui_config.sh), so the icon file must physically exist in the frontend
|
||||
# icons dir or the apps page falls back to the default icon.
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ webuiContainerSetup()
|
||||
webuiGenerateLibrePortalConfig
|
||||
fi
|
||||
# The patch path rewrites apps.json (which references
|
||||
# icons/apps/<app>.svg) but doesn't place the icon file — sync it here
|
||||
# /icons/apps/<app>.svg) but doesn't place the icon file — sync it here
|
||||
# so the apps page never points at a missing icon. The full regen
|
||||
# copies icons itself; this is cheap and idempotent regardless.
|
||||
if declare -F webuiSyncAppIcon >/dev/null 2>&1; then
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user