Convert the remaining sections off the legacy ?= query form to clean paths, matching the Admin area: /apps/<category> (was /apps?=<category>) /app/<name>?tab=&task= (was /app?=<name>&tab=&task=) /tasks/<category>?task= (was /tasks?=<category>&task=) /backup/<tab> (was /backup?=<tab>) Builders updated everywhere (sidebar, dashboard, notifications, tasks, apps, app tabs, task-actions, setup watcher); parsers now read the resource from the path with the legacy ?= kept as a fallback so old links/bookmarks still work (server already serves index.html at any depth). Route table gains /apps* and orders it before /app* (since '/apps' startsWith '/app'); active-nav and config/apps data-loading recognise the new paths. Tab/task remain ordinary query params (modifiers, not the primary resource). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
408 lines
13 KiB
JavaScript
Executable File
408 lines
13 KiB
JavaScript
Executable File
/**
|
|
* Task Actions - Action implementations for individual task operations
|
|
* Handles app installations, management operations, etc.
|
|
*/
|
|
|
|
class TaskActions {
|
|
constructor(tasksManager, commands) {
|
|
this.tasksManager = tasksManager;
|
|
this.commands = commands;
|
|
}
|
|
|
|
/**
|
|
* Install an application
|
|
*/
|
|
async installApp(appName, config = '', resetNetwork = false) {
|
|
try {
|
|
this.commands.validateCommand('install', { appName });
|
|
|
|
if (resetNetwork) {
|
|
const parts = ['libreportal', 'app', 'install', appName];
|
|
if (config) parts.push(`'${config.replace(/'/g, "'\\''")}'`);
|
|
parts.push('--reset-network');
|
|
return await this.executeTask('install', appName, parts.join(' '));
|
|
}
|
|
return await this.executeTask('install', appName, config);
|
|
} catch (error) {
|
|
throw new Error(`Failed to install ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uninstall an application
|
|
*/
|
|
async uninstallApp(appName, deleteImage = false, deleteTasks = false) {
|
|
try {
|
|
this.commands.validateCommand('uninstall', { appName });
|
|
// Build a verbatim command when the user opted into any flag so the
|
|
// --delete-images / --delete-tasks switches survive executeTask's
|
|
// standard command-builder (which would otherwise quote them as config args).
|
|
const flags = [];
|
|
if (deleteImage) flags.push('--delete-images');
|
|
if (deleteTasks) flags.push('--delete-tasks');
|
|
if (flags.length) {
|
|
const command = `libreportal app uninstall ${appName} ${flags.join(' ')}`;
|
|
return await this.executeTask('uninstall', appName, command);
|
|
}
|
|
return await this.executeTask('uninstall', appName);
|
|
} catch (error) {
|
|
throw new Error(`Failed to uninstall ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart an application
|
|
*/
|
|
async restartApp(appName) {
|
|
try {
|
|
this.commands.validateCommand('restart', { appName });
|
|
return await this.executeTask('restart', appName);
|
|
} catch (error) {
|
|
throw new Error(`Failed to restart ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start an application
|
|
*/
|
|
async startApp(appName) {
|
|
try {
|
|
this.commands.validateCommand('start', { appName });
|
|
return await this.executeTask('start', appName);
|
|
} catch (error) {
|
|
throw new Error(`Failed to start ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop an application
|
|
*/
|
|
async stopApp(appName) {
|
|
try {
|
|
this.commands.validateCommand('stop', { appName });
|
|
return await this.executeTask('stop', appName);
|
|
} catch (error) {
|
|
throw new Error(`Failed to stop ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create backup for an application
|
|
*/
|
|
async backupApp(appName, customPassword = '') {
|
|
try {
|
|
this.commands.validateCommand('backup', { appName });
|
|
return await this.executeTask('backup', appName, customPassword);
|
|
} catch (error) {
|
|
throw new Error(`Failed to backup ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async restoreApp(appName, location, backupFile, password) {
|
|
try {
|
|
if (!appName) throw new Error('appName is required');
|
|
if (!backupFile) throw new Error('backupFile is required');
|
|
|
|
const parts = ['libreportal', 'app', 'restore', appName];
|
|
if (location) parts.push(location);
|
|
parts.push(backupFile);
|
|
if (password) parts.push(password);
|
|
return await this.executeTask('restore', appName, parts.join(' '));
|
|
} catch (error) {
|
|
throw new Error(`Failed to restore ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete backup file for an application
|
|
*/
|
|
async deleteBackup(appName, backupFile, deleteRemote1 = 'false', deleteRemote2 = 'false') {
|
|
try {
|
|
// Build the command with all parameters
|
|
const command = `libreportal backup app delete ${appName} ${backupFile} ${deleteRemote1} ${deleteRemote2}`;
|
|
|
|
// Create task directly with the full command
|
|
// createTask will handle monitoring
|
|
const task = await this.tasksManager.taskManager.createTask(command, 'delete', appName, backupFile);
|
|
|
|
// Emit taskCreated event for AppTabbedManager to track the task
|
|
// This is needed for tab re-enabling on task completion
|
|
window.dispatchEvent(new CustomEvent('taskCreated', {
|
|
detail: {
|
|
taskId: task.id,
|
|
appName: appName,
|
|
action: 'delete',
|
|
timestamp: Date.now()
|
|
}
|
|
}));
|
|
|
|
return task;
|
|
} catch (error) {
|
|
throw new Error(`Failed to delete backup ${backupFile}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete all backup files for an application
|
|
*/
|
|
async deleteAllBackups(appName, deleteRemote1 = 'false', deleteRemote2 = 'false') {
|
|
try {
|
|
// Build the command with all parameters
|
|
const command = `libreportal backup app delete_all ${appName} ${deleteRemote1} ${deleteRemote2}`;
|
|
|
|
// Create task directly with the full command
|
|
// createTask will handle monitoring
|
|
const task = await this.tasksManager.taskManager.createTask(command, 'delete_all', appName, 'all');
|
|
|
|
// Emit taskCreated event for AppTabbedManager to track the task
|
|
// This is needed for tab re-enabling on task completion
|
|
window.dispatchEvent(new CustomEvent('taskCreated', {
|
|
detail: {
|
|
taskId: task.id,
|
|
appName: appName,
|
|
action: 'delete_all',
|
|
timestamp: Date.now()
|
|
}
|
|
}));
|
|
|
|
return task;
|
|
} catch (error) {
|
|
throw new Error(`Failed to delete all backups for ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update application configuration
|
|
*/
|
|
async updateConfig(appName) {
|
|
try {
|
|
this.commands.validateCommand('update_config', { appName });
|
|
|
|
await this.executeTask('update_config', appName);
|
|
return;
|
|
} catch (error) {
|
|
throw new Error(`Failed to update config for ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async configUpdate(changes) {
|
|
try {
|
|
if (!changes) throw new Error('No changes to save');
|
|
const command = `libreportal config update ${changes}`;
|
|
return await this.executeTask('config_update', 'system', command);
|
|
} catch (error) {
|
|
throw new Error(`Failed to update configuration: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run a per-app tool from the Tools tab. Builds:
|
|
* libreportal app tool <appName> <toolName> '<argsString>'
|
|
* argsString is pipe-encoded: key=value|key=value (matches the install
|
|
* config pattern so the bash side can use the same parser).
|
|
*/
|
|
async runTool(appName, toolName, toolArgs = '', toolLabel = '') {
|
|
try {
|
|
if (!appName) throw new Error('appName is required');
|
|
if (!toolName) throw new Error('toolName is required');
|
|
|
|
const safeTool = String(toolName).replace(/[^A-Za-z0-9_.-]/g, '');
|
|
if (!safeTool) throw new Error('Invalid tool name');
|
|
|
|
const parts = ['libreportal', 'app', 'tool', appName, safeTool];
|
|
if (toolArgs) {
|
|
parts.push(`'${String(toolArgs).replace(/'/g, "'\\''")}'`);
|
|
}
|
|
return await this.executeTask('tool', appName, parts.join(' '), toolLabel);
|
|
} catch (error) {
|
|
throw new Error(`Failed to run tool ${toolName} on ${appName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* System operations
|
|
*/
|
|
async systemUpdate() {
|
|
try {
|
|
await this.executeTask('system_update', 'system');
|
|
return;
|
|
} catch (error) {
|
|
throw new Error(`Failed to update system: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a task object
|
|
*/
|
|
createTask(type, command, metadata = {}) {
|
|
const task = {
|
|
id: Date.now().toString(),
|
|
command: command,
|
|
type: type,
|
|
status: 'running',
|
|
createdAt: new Date().toISOString(),
|
|
output: `Starting ${type} operation...`,
|
|
error: null,
|
|
...metadata
|
|
};
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Execute a task with enhanced event emission
|
|
*/
|
|
async executeTask(action, appName, config = '', displayLabel = '') {
|
|
try {
|
|
// If config is a full `libreportal …` command, use it verbatim. Otherwise
|
|
// build the standard form and single-quote the config arg.
|
|
let command;
|
|
if (config && config.startsWith('libreportal')) {
|
|
command = config;
|
|
} else {
|
|
command = `libreportal app ${action} ${appName}`;
|
|
if (config) command += ` '${config.replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
// Create task — POSTs to /api/tasks and returns the authoritative task.
|
|
const task = await this.tasksManager.taskManager.createTask(command, action, appName, config);
|
|
|
|
// Hook the UI side-effects (auto-expand, log streaming, status interval).
|
|
// The bus also delivers `taskCreated` from the SSE feed; AppTabbedManager
|
|
// dedupes by appName|action so the double-fire is harmless.
|
|
if (this.tasksManager && typeof this.tasksManager.monitorTask === 'function') {
|
|
this.tasksManager.monitorTask(task.id, appName, action);
|
|
}
|
|
|
|
window.dispatchEvent(new CustomEvent('taskCreated', {
|
|
detail: {
|
|
taskId: task.id,
|
|
appName: appName,
|
|
action: action,
|
|
timestamp: Date.now()
|
|
}
|
|
}));
|
|
|
|
// Show success notification
|
|
let appData = null;
|
|
try {
|
|
appData = this.commands.getAppData ? this.commands.getAppData(appName) : null;
|
|
} catch (error) {
|
|
console.warn('Could not get app data:', error);
|
|
}
|
|
const appIcon = appData?.icon || `icons/apps/${task.app}.svg`;
|
|
|
|
let taskUrl;
|
|
const currentUrl = window.location.href;
|
|
|
|
if (currentUrl.includes('/app/') && appName) {
|
|
taskUrl = `/app/${appName}?tab=tasks&task=${task.id}`;
|
|
} else {
|
|
taskUrl = `/tasks/all?task=${task.id}`;
|
|
}
|
|
|
|
if (window.notificationSystem) {
|
|
const displayName = window.getAppDisplayName ? window.getAppDisplayName(appName) : appName;
|
|
const typeIcon = (window.tasksManager && window.tasksManager.getTaskTypeIcon
|
|
? window.tasksManager.getTaskTypeIcon({ type: action })
|
|
: null)?.icon || '';
|
|
const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;
|
|
const headline = displayLabel
|
|
? `${displayLabel} task started!`
|
|
: `${action.charAt(0).toUpperCase() + action.slice(1)} task started!`;
|
|
window.notificationSystem.show(
|
|
`<strong>App: ${displayName}</strong><br>${headline}`,
|
|
'success',
|
|
appName,
|
|
taskUrl,
|
|
appIcon,
|
|
customIcon
|
|
);
|
|
}
|
|
|
|
return task;
|
|
} catch (error) {
|
|
console.error(`❌ Failed to execute ${action} task for ${appName}:`, error);
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.error(`Failed to start ${action} task for ${appName}: ${error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute task monitoring (separated to avoid duplicate task creation)
|
|
*/
|
|
async executeTaskMonitoring(task, appName, action) {
|
|
// Emit task creation event for AppTabbedManager
|
|
window.dispatchEvent(new CustomEvent('taskCreated', {
|
|
detail: {
|
|
taskId: task.id,
|
|
appName: appName,
|
|
action: action,
|
|
timestamp: Date.now()
|
|
}
|
|
}));
|
|
|
|
// console.log(`✅ Task created: ${task.id} for ${action} on ${appName}`);
|
|
|
|
// Set up monitoring for this specific task
|
|
this.tasksManager.monitorTask(task.id, appName, action);
|
|
|
|
// Try to get app data for better notifications
|
|
let appData = null;
|
|
try {
|
|
appData = this.commands.getAppData ? this.commands.getAppData(appName) : null;
|
|
} catch (error) {
|
|
console.warn('Could not get app data:', error);
|
|
}
|
|
const appIcon = appData?.icon || `icons/apps/${task.app}.svg`;
|
|
|
|
// Smart URL generation - always include app name
|
|
let taskUrl;
|
|
const currentUrl = window.location.href;
|
|
// console.log('🔍 TaskActions: Current URL:', currentUrl);
|
|
|
|
// Always generate URL with app name for proper navigation
|
|
if (currentUrl.includes('/app/') && appName) {
|
|
// We're on an app page, maintain app context
|
|
taskUrl = `/app/${appName}?tab=tasks&task=${task.id}`;
|
|
} else {
|
|
// We're on main tasks page, use normal URL
|
|
taskUrl = `/tasks/all?task=${task.id}`;
|
|
}
|
|
|
|
// Show success notification with app icon and direct link
|
|
if (window.notificationSystem) {
|
|
const displayName = window.getAppDisplayName ? window.getAppDisplayName(appName) : appName;
|
|
window.notificationSystem.show(
|
|
`<strong>App: ${displayName}</strong><br>
|
|
${action.charAt(0).toUpperCase() + action.slice(1)} task started!`,
|
|
'success',
|
|
appName,
|
|
taskUrl,
|
|
appIcon
|
|
);
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Update task in the manager
|
|
*/
|
|
updateTaskInManager(updatedTask) {
|
|
const taskIndex = this.tasksManager.tasks.findIndex(t => t.id === updatedTask.id);
|
|
if (taskIndex !== -1) {
|
|
this.tasksManager.tasks[taskIndex] = updatedTask;
|
|
this.tasksManager.renderTasks();
|
|
this.tasksManager.updateStats();
|
|
this.tasksManager.updateSidebarCounts();
|
|
this.tasksManager.generateAppCategories();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export for use
|
|
window.TaskActions = TaskActions;
|