librelad 9ca5cc6c7c feat(system): full, deletable images list on the Storage page
Replaces the read-only "Largest images" top-10 table with a Tasks-style list of
ALL Docker images, with select-one / select-multiple / clear-all removal that
mirrors the Tasks page UX (row checkboxes, master select-all, a button that
morphs Clear All ↔ Delete Selected (N), an eo confirm modal).

Deletion routes through the task system, NOT a new web API: a new
`libreportal system image rm [--force] <ids>` CLI subcommand (validates each
ref, loops runFileOp docker image rm, reports a tally) is invoked via the
system_image_rm task action — same pattern as Reclaim. The web backend change
is read-only (uncap the existing /storage image list). In-use images are
skipped by default with an opt-in "force-remove" toggle (warned). The page
stays put, toasts, and refreshes on the task's completion event.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-28 21:32:29 +01:00

234 lines
6.2 KiB
JavaScript
Executable File

/**
* Task Router - Action routing and dispatch for individual tasks
* Handles action routing, queuing, and error management
*/
class TaskRouter {
constructor(tasksManager, actions) {
this.tasksManager = tasksManager;
this.actions = actions;
this.commandQueue = [];
this.isProcessing = false;
}
/**
* Route action to appropriate handler
*/
async routeAction(action, params = {}) {
try {
//console.log(`🎯 Routing action: ${action}`, params);
switch (action) {
case 'install':
return await this.actions.installApp(params.appName, params.config, params.resetNetwork);
case 'uninstall':
return await this.actions.uninstallApp(params.appName, params.deleteImage, params.deleteTasks);
case 'restart':
return await this.actions.restartApp(params.appName);
case 'start':
return await this.actions.startApp(params.appName);
case 'stop':
return await this.actions.stopApp(params.appName);
case 'backup':
return await this.actions.backupApp(params.appName, params.customPassword);
case 'restore':
return await this.actions.restoreApp(
params.appName,
params.location,
params.backupFile,
params.password
);
case 'delete':
return await this.actions.deleteBackup(params.appName, params.backupFile, params.deleteRemote1, params.deleteRemote2);
case 'delete_all':
return await this.actions.deleteAllBackups(params.appName, params.deleteRemote1, params.deleteRemote2);
case 'update_config':
return await this.actions.updateConfig(params.appName);
case 'config_update':
return await this.actions.configUpdate(params.changes);
case 'system_update':
return await this.actions.systemUpdate();
case 'system_reclaim':
return await this.actions.systemReclaim();
case 'system_image_rm':
return await this.actions.removeImages(params.ids, params.force);
case 'tool':
return await this.actions.runTool(params.appName, params.toolName, params.toolArgs, params.toolLabel);
default:
throw new Error(`Unknown action: ${action}`);
}
} catch (error) {
console.error(`❌ Action routing failed: ${error.message}`);
throw error;
}
}
/**
* Queue action for execution
*/
queueAction(action, params = {}) {
const queuedTask = {
id: Date.now().toString(),
action: action,
params: params,
timestamp: new Date().toISOString(),
status: 'queued'
};
this.commandQueue.push(queuedTask);
//console.log(`📋 Queued action: ${action}`, params);
// Start processing if not already running
if (!this.isProcessing) {
this.processQueue();
}
return queuedTask.id;
}
/**
* Process the command queue
*/
async processQueue() {
if (this.isProcessing || this.commandQueue.length === 0) {
return;
}
this.isProcessing = true;
//console.log('🔄 Processing command queue...');
while (this.commandQueue.length > 0) {
const queuedTask = this.commandQueue.shift();
try {
//console.log(`⚡ Executing queued action: ${queuedTask.action}`);
await this.routeAction(queuedTask.action, queuedTask.params);
queuedTask.status = 'completed';
//console.log(`✅ Completed queued action: ${queuedTask.action}`);
} catch (error) {
queuedTask.status = 'failed';
queuedTask.error = error.message;
console.error(`❌ Failed queued action: ${queuedTask.action}`, error);
if (window.notificationSystem) {
// Pull the per-action icon if we know it; falls back to the
// generic error SVG via customIcon=null.
const typeIcon = (window.tasksManager && window.tasksManager.getTaskTypeIcon
? window.tasksManager.getTaskTypeIcon({ type: queuedTask.action })
: null)?.icon || '';
const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;
window.notificationSystem.show(
`Queued action failed: ${error.message}`,
'error',
null,
null,
null,
customIcon
);
}
}
}
this.isProcessing = false;
//console.log('✅ Command queue processing complete');
}
/**
* Get queue status
*/
getQueueStatus() {
return {
isProcessing: this.isProcessing,
queueLength: this.commandQueue.length,
queuedTasks: [...this.commandQueue]
};
}
/**
* Clear the queue
*/
clearQueue() {
const clearedCount = this.commandQueue.length;
this.commandQueue = [];
if (window.notificationSystem) {
const customIcon = '<span style="font-size:18px;line-height:1;">🗑️</span>';
window.notificationSystem.show(
`Cleared ${clearedCount} queued actions`,
'info',
null,
null,
null,
customIcon
);
}
return clearedCount;
}
/**
* Get available actions
*/
getAvailableActions() {
return [
'install',
'uninstall',
'restart',
'start',
'stop',
'backup',
'delete',
'delete_all',
'update_config',
'system_update',
'tool'
];
}
/**
* Validate action parameters
*/
validateAction(action, params) {
const requiredParams = {
install: ['appName'],
uninstall: ['appName'],
restart: ['appName'],
start: ['appName'],
stop: ['appName'],
backup: ['appName'],
delete: ['appName', 'backupFile'],
update_config: ['appName'],
tool: ['appName', 'toolName']
};
const required = requiredParams[action] || [];
const missing = required.filter(param => !params[param]);
if (missing.length > 0) {
throw new Error(`Missing required parameters for ${action}: ${missing.join(', ')}`);
}
return true;
}
}
// Export for use
window.TaskRouter = TaskRouter;