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>
|
</button>
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<div class="libreportal-logo">
|
<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>
|
</div>
|
||||||
<div class="mobile-drawer" id="mobile-drawer">
|
<div class="mobile-drawer" id="mobile-drawer">
|
||||||
|
|||||||
@ -652,7 +652,7 @@ class AppsManager {
|
|||||||
if (onHiddenTab) {
|
if (onHiddenTab) {
|
||||||
window.appTabbedManager.switchTab('config');
|
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
|
// Ensure absolute path from root
|
||||||
if (!icon.startsWith('/')) {
|
if (!icon.startsWith('/')) {
|
||||||
@ -672,7 +672,7 @@ class AppsManager {
|
|||||||
const headerHTML = `
|
const headerHTML = `
|
||||||
<div class="app-info">
|
<div class="app-info">
|
||||||
<div class="app-card-icon" style="width: 80px; height: 80px; min-width: 80px;">
|
<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>
|
||||||
<div class="app-details">
|
<div class="app-details">
|
||||||
<h2>${shortName}</h2>
|
<h2>${shortName}</h2>
|
||||||
@ -1701,7 +1701,7 @@ class AppsManager {
|
|||||||
card.dataset.search = searchHaystack;
|
card.dataset.search = searchHaystack;
|
||||||
|
|
||||||
const appName = app.command.split(' ').pop();
|
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
|
// Ensure absolute path from root
|
||||||
if (!icon.startsWith('/')) {
|
if (!icon.startsWith('/')) {
|
||||||
@ -2476,7 +2476,7 @@ class AppsManager {
|
|||||||
async getFieldMappings() {
|
async getFieldMappings() {
|
||||||
try {
|
try {
|
||||||
// Load from apps folder (static file)
|
// 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();
|
const data = await response.json();
|
||||||
//// // console.log('✅ Loaded field mappings from apps folder');
|
//// // console.log('✅ Loaded field mappings from apps folder');
|
||||||
return data.fields || data;
|
return data.fields || data;
|
||||||
|
|||||||
@ -577,7 +577,7 @@ class BackupPage {
|
|||||||
const command = a.command || '';
|
const command = a.command || '';
|
||||||
return command.endsWith(` ${slug}`) || a.name?.toLowerCase() === slug.toLowerCase();
|
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;
|
if (!icon.startsWith('/')) icon = '/' + icon;
|
||||||
const displayName = (typeof window.getAppDisplayName === 'function')
|
const displayName = (typeof window.getAppDisplayName === 'function')
|
||||||
? window.getAppDisplayName(slug)
|
? window.getAppDisplayName(slug)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ window.ConfigValidator = function() {
|
|||||||
|
|
||||||
// Check if unified config file exists (file existence check only)
|
// Check if unified config file exists (file existence check only)
|
||||||
const configFiles = [
|
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) {
|
for (const config of configFiles) {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ function renderInstalledApps() {
|
|||||||
|
|
||||||
function createInstalledAppCard(app) {
|
function createInstalledAppCard(app) {
|
||||||
const appName = app.command.split(' ').pop();
|
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;
|
if (!icon.startsWith('/')) icon = '/' + icon;
|
||||||
const shortName = app.name.split(' - ')[0].trim();
|
const shortName = app.name.split(' - ')[0].trim();
|
||||||
|
|
||||||
|
|||||||
@ -103,7 +103,7 @@ class NotificationSystem {
|
|||||||
if (appIcon) {
|
if (appIcon) {
|
||||||
content += `
|
content += `
|
||||||
<div class="notification-app-icon">
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -290,7 +290,7 @@ async configUpdate(changes) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not get app data:', 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;
|
let taskUrl;
|
||||||
const currentUrl = window.location.href;
|
const currentUrl = window.location.href;
|
||||||
@ -356,7 +356,7 @@ async configUpdate(changes) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not get app data:', 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
|
// Smart URL generation - always include app name
|
||||||
let taskUrl;
|
let taskUrl;
|
||||||
|
|||||||
@ -331,7 +331,7 @@ class TaskCommands {
|
|||||||
// Fallback: create minimal app data
|
// Fallback: create minimal app data
|
||||||
return {
|
return {
|
||||||
name: appName,
|
name: appName,
|
||||||
icon: `icons/apps/${appName}.svg`,
|
icon: `/icons/apps/${appName}.svg`,
|
||||||
command: `libreportal app install ${appName}`
|
command: `libreportal app install ${appName}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1035,7 +1035,7 @@ class TasksManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default icon path
|
// Default icon path
|
||||||
return `icons/apps/${task.app}.svg`;
|
return `/icons/apps/${task.app}.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStats() {
|
updateStats() {
|
||||||
@ -1343,7 +1343,7 @@ class TasksManager {
|
|||||||
'info',
|
'info',
|
||||||
appName,
|
appName,
|
||||||
taskUrl,
|
taskUrl,
|
||||||
`icons/apps/${appName}.svg`,
|
`/icons/apps/${appName}.svg`,
|
||||||
customIcon
|
customIcon
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1458,7 +1458,7 @@ class TasksManager {
|
|||||||
const url = (onAppPage && appName)
|
const url = (onAppPage && appName)
|
||||||
? `/app/${appName}?tab=tasks&task=${taskId}`
|
? `/app/${appName}?tab=tasks&task=${taskId}`
|
||||||
: `/tasks/all?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
|
// Match the per-action emoji used in the task list rows (see
|
||||||
// `getTaskTypeIcon`). Passed as the 6th `customIcon` arg so the
|
// `getTaskTypeIcon`). Passed as the 6th `customIcon` arg so the
|
||||||
|
|||||||
@ -327,7 +327,7 @@ class SetupWizard {
|
|||||||
const app = manifest.get(slug) || {};
|
const app = manifest.get(slug) || {};
|
||||||
const name = app.title || app.name || fallback.name;
|
const name = app.title || app.name || fallback.name;
|
||||||
const desc = app.description || fallback.description;
|
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
|
// Sub-option lives OUTSIDE the parent label (nested <label> behaviour
|
||||||
// is messy — clicks would toggle both checkboxes). Both labels are
|
// is messy — clicks would toggle both checkboxes). Both labels are
|
||||||
// siblings inside a wrapper div; CSS connects them visually.
|
// siblings inside a wrapper div; CSS connects them visually.
|
||||||
@ -342,7 +342,7 @@ class SetupWizard {
|
|||||||
<input type="checkbox" data-app="${slug}" ${defaultChecked ? 'checked' : ''}>
|
<input type="checkbox" data-app="${slug}" ${defaultChecked ? 'checked' : ''}>
|
||||||
<div class="setup-app-icon-wrap">
|
<div class="setup-app-icon-wrap">
|
||||||
<img src="${icon}" alt="${name}" class="setup-app-icon"
|
<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>
|
||||||
<div class="setup-app-info">
|
<div class="setup-app-info">
|
||||||
<div class="setup-app-name">${name}</div>
|
<div class="setup-app-name">${name}</div>
|
||||||
|
|||||||
@ -118,7 +118,7 @@ let categories = [];
|
|||||||
|
|
||||||
function getCategoryIcon(categoryId) {
|
function getCategoryIcon(categoryId) {
|
||||||
const category = categories.find(cat => cat.id === 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) {
|
function getCategoryName(categoryId) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
function getAppIcon(appName, appIconUrl) {
|
function getAppIcon(appName, appIconUrl) {
|
||||||
const cleanAppName = appName.replace('install_', '').replace(' ', '');
|
const cleanAppName = appName.replace('install_', '').replace(' ', '');
|
||||||
// Use icon URL from JSON if available, otherwise fall back to default
|
// 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) {
|
function getAppStatus(installed) {
|
||||||
|
|||||||
@ -212,7 +212,7 @@ EOF
|
|||||||
fields+=(" \"backup_live_capable\": $backup_live_capable")
|
fields+=(" \"backup_live_capable\": $backup_live_capable")
|
||||||
[[ -n "$url" ]] && fields+=(" \"url\": \"$url\"")
|
[[ -n "$url" ]] && fields+=(" \"url\": \"$url\"")
|
||||||
[[ -n "$description" ]] && fields+=(" \"description\": \"$description\"")
|
[[ -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]")
|
[[ -n "$services_json" ]] && fields+=(" \"services\": [$services_json]")
|
||||||
if [[ -n "$config_vars" ]]; then
|
if [[ -n "$config_vars" ]]; then
|
||||||
fields+=(" \"config\": {"$'\n'"${config_vars%,}"$'\n'" }")
|
fields+=(" \"config\": {"$'\n'"${config_vars%,}"$'\n'" }")
|
||||||
|
|||||||
@ -26,67 +26,67 @@ webuiCreateAppsCategories() {
|
|||||||
{
|
{
|
||||||
"id": "recommended",
|
"id": "recommended",
|
||||||
"name": "Recommended",
|
"name": "Recommended",
|
||||||
"icon": "icons/categories/recommended.svg",
|
"icon": "/icons/categories/recommended.svg",
|
||||||
"description": "Hand-picked apps to round out your install"
|
"description": "Hand-picked apps to round out your install"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "communication",
|
"id": "communication",
|
||||||
"name": "Communication",
|
"name": "Communication",
|
||||||
"icon": "icons/categories/communication.svg",
|
"icon": "/icons/categories/communication.svg",
|
||||||
"description": "Communication and collaboration tools"
|
"description": "Communication and collaboration tools"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "development",
|
"id": "development",
|
||||||
"name": "Development",
|
"name": "Development",
|
||||||
"icon": "icons/categories/development.svg",
|
"icon": "/icons/categories/development.svg",
|
||||||
"description": "Development tools and version control"
|
"description": "Development tools and version control"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "knowledge",
|
"id": "knowledge",
|
||||||
"name": "Knowledge",
|
"name": "Knowledge",
|
||||||
"icon": "icons/categories/knowledge.svg",
|
"icon": "/icons/categories/knowledge.svg",
|
||||||
"description": "Knowledge management and wikis"
|
"description": "Knowledge management and wikis"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "media",
|
"id": "media",
|
||||||
"name": "Media",
|
"name": "Media",
|
||||||
"icon": "icons/categories/media.svg",
|
"icon": "/icons/categories/media.svg",
|
||||||
"description": "Media streaming and entertainment"
|
"description": "Media streaming and entertainment"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "monitoring",
|
"id": "monitoring",
|
||||||
"name": "Monitoring",
|
"name": "Monitoring",
|
||||||
"icon": "icons/categories/monitoring.svg",
|
"icon": "/icons/categories/monitoring.svg",
|
||||||
"description": "Metrics, dashboards and observability"
|
"description": "Metrics, dashboards and observability"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "networking",
|
"id": "networking",
|
||||||
"name": "Networking",
|
"name": "Networking",
|
||||||
"icon": "icons/categories/networking.svg",
|
"icon": "/icons/categories/networking.svg",
|
||||||
"description": "Network tools and services"
|
"description": "Network tools and services"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "productivity",
|
"id": "productivity",
|
||||||
"name": "Productivity",
|
"name": "Productivity",
|
||||||
"icon": "icons/categories/productivity.svg",
|
"icon": "/icons/categories/productivity.svg",
|
||||||
"description": "Productivity and business tools"
|
"description": "Productivity and business tools"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "security",
|
"id": "security",
|
||||||
"name": "Security",
|
"name": "Security",
|
||||||
"icon": "icons/categories/security.svg",
|
"icon": "/icons/categories/security.svg",
|
||||||
"description": "Security and privacy tools"
|
"description": "Security and privacy tools"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "storage",
|
"id": "storage",
|
||||||
"name": "Storage",
|
"name": "Storage",
|
||||||
"icon": "icons/categories/storage.svg",
|
"icon": "/icons/categories/storage.svg",
|
||||||
"description": "File storage and sharing solutions"
|
"description": "File storage and sharing solutions"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "system",
|
"id": "system",
|
||||||
"name": "System",
|
"name": "System",
|
||||||
"icon": "icons/categories/system.svg",
|
"icon": "/icons/categories/system.svg",
|
||||||
"description": "LibrePortal itself and platform-level services"
|
"description": "LibrePortal itself and platform-level services"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Sync app icons from container template dirs to the LibrePortal frontend.
|
# 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
|
# 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.
|
# icons dir or the apps page falls back to the default icon.
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ webuiContainerSetup()
|
|||||||
webuiGenerateLibrePortalConfig
|
webuiGenerateLibrePortalConfig
|
||||||
fi
|
fi
|
||||||
# The patch path rewrites apps.json (which references
|
# 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
|
# so the apps page never points at a missing icon. The full regen
|
||||||
# copies icons itself; this is cheap and idempotent regardless.
|
# copies icons itself; this is cheap and idempotent regardless.
|
||||||
if declare -F webuiSyncAppIcon >/dev/null 2>&1; then
|
if declare -F webuiSyncAppIcon >/dev/null 2>&1; then
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user