A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys, Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun VPN routing, and a web dashboard to manage it all. Free & open forever to self-host; optional paid hosted services fund it. See PROMISE.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
361 lines
10 KiB
JavaScript
Executable File
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;
|