librelad 152d9c5d28 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>
2026-05-24 23:20:42 +01:00

361 lines
10 KiB
JavaScript
Executable File

/**
* Task Commands - Pre-built command templates and execution
* Handles SSH command execution and validation for individual tasks
*/
class TaskCommands {
constructor() {
this.commandTemplates = {
// App Commands (✅ IMPLEMENTED)
install: 'libreportal app install {appName} {config}',
uninstall: 'libreportal app uninstall {appName}',
restart: 'libreportal app restart {appName}',
start: 'libreportal app start {appName}',
stop: 'libreportal app stop {appName}',
backup: 'libreportal app backup {appName}',
status: 'libreportal app status {appName}',
// Docker Compose Management (✅ IMPLEMENTED)
up: 'libreportal app up {appName}',
down: 'libreportal app down {appName}',
reload: 'libreportal app reload {appName}',
// System Commands (✅ IMPLEMENTED)
system_status: 'libreportal system status',
system_update: 'libreportal system update',
system_reset: 'libreportal system reset',
// Future Commands (❌ NOT YET IMPLEMENTED)
// restore: 'libreportal app restore {appName} {backupId}',
// update_config: 'libreportal config update {appName}',
// system_info: 'libreportal system info',
// system_disk: 'libreportal system disk',
// system_memory: 'libreportal system memory'
};
this.commandStatus = {
// ✅ Available in CLI
install: 'implemented',
uninstall: 'implemented',
restart: 'implemented',
start: 'implemented',
stop: 'implemented',
backup: 'implemented',
status: 'implemented',
up: 'implemented',
down: 'implemented',
reload: 'implemented',
system_status: 'implemented',
system_update: 'implemented',
system_reset: 'implemented',
// ❌ Not yet implemented in CLI
restore: 'not_implemented',
update_config: 'not_implemented',
system_info: 'not_implemented',
system_disk: 'not_implemented',
system_memory: 'not_implemented'
};
}
/**
* Generate command from template with parameters
*/
generateCommand(type, params = {}) {
const template = this.commandTemplates[type];
if (!template) {
throw new Error(`Unknown command type: ${type}`);
}
let command = template;
Object.keys(params).forEach(key => {
const value = params[key] || '';
command = command.replace(`{${key}}`, value);
});
// Clean up double spaces and trailing spaces
command = command.replace(/\s+/g, ' ').trim();
return command;
}
/**
* Check if command is implemented in CLI
*/
isCommandImplemented(type) {
return this.commandStatus[type] === 'implemented';
}
/**
* Get command status
*/
getCommandStatus(type) {
return this.commandStatus[type] || 'unknown';
}
/**
* Get only implemented commands
*/
getImplementedCommands() {
return Object.keys(this.commandStatus).filter(cmd => this.commandStatus[cmd] === 'implemented');
}
/**
* Get all commands with their status
*/
getAllCommandsWithStatus() {
return Object.keys(this.commandStatus).map(cmd => ({
command: cmd,
template: this.commandTemplates[cmd],
status: this.commandStatus[cmd]
}));
}
/**
* Validate command and check if implemented
*/
validateCommand(type, params) {
// Check if command exists
const template = this.commandTemplates[type];
if (!template) {
throw new Error(`Unknown command type: ${type}`);
}
// Check if command is implemented
if (!this.isCommandImplemented(type)) {
throw new Error(`Command '${type}' is not yet implemented in the CLI system`);
}
const requiredParams = {
install: ['appName'], // config is optional
uninstall: ['appName'],
restart: ['appName'],
start: ['appName'],
stop: ['appName'],
backup: ['appName'],
status: ['appName'],
up: ['appName'],
down: ['appName'],
reload: ['appName']
};
const required = requiredParams[type] || [];
const missing = required.filter(param => !params[param]);
if (missing.length > 0) {
throw new Error(`Missing required parameters: ${missing.join(', ')}`);
}
return true;
}
/**
* Execute command via API
*/
async executeCommand(command, taskId) {
try {
//// // console.log(`🚀 Executing command: ${command}`);
// Add task to local queue using generic endpoint
const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const taskFileName = `${taskId}.json`;
const taskFilePath = `tasks/queue/${taskFileName}`;
// console.log('🔍 Creating task file:', taskFilePath);
// console.log('🔍 Task ID:', taskId);
const task = {
id: taskId,
type: 'install',
app: this.extractAppName(command),
command: command,
config: null,
status: 'queued',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// console.log('🔍 Task object:', task);
const response = await fetch('/write-file', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: taskFilePath,
content: JSON.stringify(task, null, 2)
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error(`❌ API Error Response:`, errorData);
throw new Error(`Command execution failed: ${response.statusText} - ${errorData.error || 'Unknown error'}`);
}
const result = await response.json();
//// // console.log(`✅ Task queued successfully:`, result);
return {
success: true,
output: `Task queued: ${command}`,
taskId: result.taskId,
exitCode: 0,
queued: true
};
} catch (error) {
console.error(`❌ Command execution failed:`, error);
throw new Error(`Command execution failed: ${error.message}`);
}
}
/**
* Extract app name from command
*/
extractAppName(command) {
const match = command.match(/libreportal app (\w+) (.+)/);
return match ? match[2] : 'unknown';
}
/**
* Execute command via file-based task system
*/
async executeCommand(command, taskId) {
//// // console.log(`🔧 Creating task for command: ${command}`);
try {
// Create task file in queue
const task = {
id: taskId,
command: command,
status: 'queued',
created: new Date().toISOString()
};
// Extract app name from command for better task tracking
const appNameMatch = command.match(/libreportal app (\w+) (\w+)/);
if (appNameMatch) {
task.app = appNameMatch[2];
task.type = appNameMatch[1];
}
// Write task file to queue
await this.createTaskFile(task);
//// // console.log(`✅ Task created: ${taskId}`);
return {
success: true,
taskId: taskId,
message: 'Task queued for execution'
};
} catch (error) {
console.error(`❌ Failed to create task:`, error);
throw new Error(`Task creation failed: ${error.message}`);
}
}
/**
* Create task file in queue directory
*/
async createTaskFile(task) {
const taskFileName = `${task.id}.json`;
const taskFilePath = `tasks/queue/${taskFileName}`;
try {
// Write task file using generic endpoint
const response = await fetch('/write-file', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: taskFilePath,
content: JSON.stringify(task, null, 2)
})
});
if (!response.ok) {
throw new Error(`Failed to create task file: ${response.statusText}`);
}
return await response.json();
} catch (error) {
// Fallback: create file directly (for development)
console.warn('API not available, using fallback method');
await this.writeTaskFileDirectly(task);
}
}
/**
* Direct task file creation (fallback method)
*/
async writeTaskFileDirectly(task) {
// This would be implemented server-side
// For now, we'll simulate the task creation
//// // console.log(`Task file created: ${task.id}.json`);
return { success: true, taskId: task.id };
}
/**
* Get available command types
*/
getCommandTypes() {
return Object.keys(this.commandTemplates);
}
/**
* Get command template for a type
*/
getCommandTemplate(type) {
return this.commandTemplates[type];
}
/**
* Get app data for icon and URL information
*/
getAppData(appName) {
if (window.apps && Array.isArray(window.apps)) {
const target = String(appName || '').toLowerCase();
const app = window.apps.find(app => {
const appCommandName = (app.command || '').split(' ').pop();
return appCommandName.toLowerCase() === target;
});
if (app) return app;
}
// Fallback: create minimal app data
return {
name: appName,
icon: `/icons/apps/${appName}.svg`,
command: `libreportal app install ${appName}`
};
}
/**
* Trigger enhanced notification with app icon and action button
*
* `customIcon` is forwarded to NotificationSystem.show so callers can
* supply a task-type emoji (install ✅, backup 💾, etc.) for the leftmost
* icon slot — same style every other task-related notification uses.
*/
triggerNotification(message, type = 'info', appName = null, appUrl = null, appIcon = null, customIcon = null) {
if (window.notificationSystem && typeof window.notificationSystem.show === 'function') {
// Use enhanced notification system
window.notificationSystem.show(message, type, appName, appUrl, appIcon, customIcon);
} else if (typeof ConfigShared !== 'undefined' && ConfigShared.showNotification) {
// Fallback to basic notification
ConfigShared.showNotification(message, type);
} else {
//// // console.log(`🔔 ${type.toUpperCase()}: ${message}`);
}
}
}
// Export for use
window.TaskCommands = TaskCommands;