License Server
Analytics
Licenses
Components
Edit Component
Update the component metadata, code, and availability.
View Component
Back
Component Name
Component name cannot be changed after creation.
Display Name
*
Version
*
Active
Description
Component: SSH Tool
Controller Code (Optional)
<?php namespace App\Http\Controllers; use App\Services\CommandGeneratorService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use phpseclib3\Net\SSH2; use phpseclib3\Crypt\PublicKeyLoader; class SshController extends Controller { /** * Display the SSH command interface. */ public function index() { $user = auth()->user(); // Check if user has SSH configured $sshConfigured = $user && $user->ssh_host && $user->ssh_username; // Define essential daily Matomo commands (only commands that exist in console_output.json) $essentialCommands = [ // Core operations 'core:archive' => 'Archive analytics data for specified sites and periods', 'core:invalidate-report-data' => 'Invalidate cached report data', 'core:clear-caches' => 'Clear all cached data', 'core:update' => 'Update Matomo to the latest version', 'core:version' => 'Display the current Matomo version', 'core:test-email' => 'Test email configuration', // Diagnostics 'diagnostics:run' => 'Run diagnostics to check system health and configuration', // Database operations 'database:optimize-archive-tables' => 'Optimize archive database tables for better performance', // Scheduled tasks 'scheduled-tasks:run' => 'Run all scheduled tasks', // Plugin management 'plugin:activate' => 'Activate a Matomo plugin', 'plugin:deactivate' => 'Deactivate a Matomo plugin', 'plugin:list' => 'List all available plugins', ]; // Get all commands and filter to only essential ones $commandGenerator = app(CommandGeneratorService::class); $allCommands = $commandGenerator->getCommands(); // Group essential commands by category $commandsByCategory = []; foreach ($essentialCommands as $commandName => $defaultDescription) { // Check if command exists in the full list if (isset($allCommands[$commandName])) { $commandData = $allCommands[$commandName]; $category = $commandData['category'] ?? 'General'; } else { // Use category from command name (e.g., "core:archive" -> "Core", "diagnostics:run" -> "Diagnostics") $categoryParts = explode(':', $commandName); $category = ucfirst($categoryParts[0] ?? 'General'); $commandData = ['description' => $defaultDescription, 'parameters' => []]; } if (!isset($commandsByCategory[$category])) { $commandsByCategory[$category] = []; } $commandsByCategory[$category][] = [ 'name' => $commandName, 'description' => $commandData['description'] ?? $defaultDescription, 'parameters' => $commandData['parameters'] ?? [], ]; } // Sort categories ksort($commandsByCategory); // Sort commands within each category foreach ($commandsByCategory as $category => &$commands) { usort($commands, fn($a, $b) => strcmp($a['name'], $b['name'])); } return view('components.ssh.index', compact('commandsByCategory', 'sshConfigured')); } /** * Get command details for form generation. */ public function getCommandDetails(Request $request) { $commandName = $request->input('command'); // Load from JSON to get original definition format $jsonFile = base_path('console_output.json'); if (!file_exists($jsonFile)) { return response()->json([ 'success' => false, 'message' => 'Command list not found' ], 404); } $jsonContent = file_get_contents($jsonFile); $data = json_decode($jsonContent, true); if (!isset($data['commands'])) { return response()->json([ 'success' => false, 'message' => 'No commands available' ], 404); } // Find the command $found = null; foreach ($data['commands'] as $cmd) { if ($cmd['name'] === $commandName) { $found = $cmd; break; } } if (!$found) { return response()->json([ 'success' => false, 'message' => 'Command not found' ], 404); } // Build parameters structure for form $parameters = []; // Add arguments if (isset($found['definition']['arguments'])) { foreach ($found['definition']['arguments'] as $argName => $argDef) { $parameters[$argName] = [ 'is_argument' => true, 'type' => $this->inferType($argDef), 'required' => $argDef['is_required'] ?? false, 'description' => $argDef['description'] ?? '', ]; } } // Add options if (isset($found['definition']['options'])) { foreach ($found['definition']['options'] as $optName => $optDef) { // Skip common system options if (in_array($optName, ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction', 'matomo-domain', 'xhprof', 'ignore-warn'])) { continue; } $acceptValue = $optDef['accept_value'] ?? true; $parameters[$optName] = [ 'is_argument' => false, 'type' => $acceptValue ? ($this->inferType($optDef)) : 'boolean', 'required' => $optDef['is_value_required'] ?? false, 'description' => $optDef['description'] ?? '', 'accept_value' => $acceptValue, ]; // Extract options from description if available // Try multiple patterns: (opt1,opt2), [opt1,opt2], or "opt1,opt2" if (isset($optDef['description'])) { $desc = $optDef['description']; $optionsList = null; // Pattern 1: (option1, option2, option3) if (preg_match('/\(([^)]+)\)/', $desc, $matches)) { $optionsList = array_map('trim', explode(',', $matches[1])); } // Pattern 2: [option1, option2, option3] elseif (preg_match('/\[([^\]]+)\]/', $desc, $matches)) { $optionsList = array_map('trim', explode(',', $matches[1])); } // Pattern 3: "option1, option2, option3" or 'option1, option2, option3' elseif (preg_match('/["\']([^"\']+)["\']/', $desc, $matches)) { $optionsList = array_map('trim', explode(',', $matches[1])); } // Pattern 4: Look for "eg. option1, option2" or "e.g. option1, option2" elseif (preg_match('/(?:eg\.|e\.g\.|example:)\s*([^\.]+)/i', $desc, $matches)) { $optionsList = array_map('trim', explode(',', $matches[1])); } // Only set options if we found valid options (not empty, not just one item that looks like description) if ($optionsList && count($optionsList) > 0) { // Filter out options that are too long (likely not actual options) // Also filter out if description mentions "comma separated" - those are free text fields $optionsList = array_filter($optionsList, function($opt) { return strlen(trim($opt)) > 0 && strlen(trim($opt)) < 50; }); // Don't create options if description mentions "comma separated" - it's a free text field $isCommaSeparated = stripos($desc, 'comma separated') !== false || stripos($desc, 'comma-separated') !== false || stripos($desc, 'separated by comma') !== false; // Only set options if: // 1. We have valid options // 2. It's not a comma-separated field // 3. We have a reasonable number of options (not too many) if (count($optionsList) > 0 && !$isCommaSeparated && count($optionsList) <= 20) { $parameters[$optName]['options'] = array_values($optionsList); } } } } } return response()->json([ 'success' => true, 'command' => [ 'name' => $commandName, 'description' => $found['description'] ?? 'No description available.', 'category' => explode(':', $commandName)[0] ?? 'General', 'parameters' => $parameters, 'examples' => $found['usage'] ?? [], ] ]); } /** * Infer parameter type from definition */ private function inferType($def): string { if (isset($def['accept_value']) && !$def['accept_value']) { return 'boolean'; } $desc = $def['description'] ?? ''; if (stripos($desc, 'integer') !== false || stripos($desc, 'number') !== false) { return 'integer'; } if (stripos($desc, 'date') !== false) { return 'date'; } if (stripos($desc, 'boolean') !== false || stripos($desc, 'flag') !== false) { return 'boolean'; } return 'string'; } /** * Execute a command via SSH. */ public function executeCommand(Request $request) { $user = auth()->user(); if (!$user || !$user->ssh_host || !$user->ssh_username) { return response()->json([ 'success' => false, 'message' => 'SSH not configured. Please configure SSH in your profile first.' ], 400); } $commandName = $request->input('command'); $args = $request->input('args', []); $opts = $request->input('opts', []); // Load command definition from JSON (same as DashboardController) $jsonFile = base_path('console_output.json'); if (!file_exists($jsonFile)) { return response()->json([ 'success' => false, 'message' => 'Command list not found.' ], 404); } $jsonContent = file_get_contents($jsonFile); $data = json_decode($jsonContent, true); if (!isset($data['commands'])) { return response()->json([ 'success' => false, 'message' => 'No commands available.' ], 404); } // Find the command $found = null; foreach ($data['commands'] as $cmd) { if ($cmd['name'] === $commandName) { $found = $cmd; break; } } if (!$found) { return response()->json([ 'success' => false, 'message' => 'Command not found.' ], 404); } // Build command string (same logic as DashboardController) $matomoFolder = $user->ssh_matomo_folder ?: '/var/www/matomo'; $cmdString = 'php ' . escapeshellarg($matomoFolder . '/console') . ' ' . escapeshellcmd($commandName); // Add arguments in order if (isset($found['definition']['arguments'])) { foreach ($found['definition']['arguments'] as $argName => $argDef) { // Try to get value by name first, then by index $argValue = null; if (isset($args[$argName]) && $args[$argName] !== '') { $argValue = $args[$argName]; } else { // Try numeric index $argKeys = array_keys($found['definition']['arguments']); $argIndex = array_search($argName, $argKeys); if ($argIndex !== false && isset($args[$argIndex]) && $args[$argIndex] !== '') { $argValue = $args[$argIndex]; } } if ($argValue !== null && $argValue !== '') { $cmdString .= ' ' . escapeshellarg($argValue); } } } // Add options if (isset($found['definition']['options'])) { foreach ($found['definition']['options'] as $optName => $optDef) { // Skip common system options if (in_array($optName, ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction', 'matomo-domain', 'xhprof', 'ignore-warn'])) { continue; } if (isset($opts[$optName])) { $value = $opts[$optName]; if (is_bool($value) && $value) { // Boolean option: --option-name $cmdString .= ' --' . escapeshellcmd($optName); } elseif ($value !== '' && $value !== false && $value !== '0' && $value !== 'false') { // Option with value: --option-name=value $cmdString .= ' --' . escapeshellcmd($optName) . '=' . escapeshellarg($value); } } } } try { // Connect via SSH $ssh = new SSH2($user->ssh_host, (int)($user->ssh_port ?: 22)); // Authenticate if ($user->ssh_use_key && $user->ssh_private_key) { $privateKey = decrypt($user->ssh_private_key); $key = PublicKeyLoader::load($privateKey); $login = $ssh->login($user->ssh_username, $key); } else { $password = $user->ssh_password ? decrypt($user->ssh_password) : null; $login = $ssh->login($user->ssh_username, $password); } if (!$login) { return response()->json([ 'success' => false, 'message' => 'SSH login failed. Please check your SSH credentials in profile settings.' ], 401); } // Execute command $output = $ssh->exec($cmdString); // Check if command failed (no output might indicate failure) if ($output === false) { return response()->json([ 'success' => false, 'message' => 'Command execution failed', 'command' => $cmdString ], 500); } return response()->json([ 'success' => true, 'output' => $output ?: 'Command executed successfully (no output)', 'command' => $cmdString ]); } catch (\Exception $e) { Log::error('SSH command execution failed: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'SSH error: ' . $e->getMessage() ], 500); } } }
PHP code for the component controller.
Routes Code (Optional)
<?php use Illuminate\Support\Facades\Route; use App\Http\Controllers\SshController; Route::middleware(['auth', 'component.visible'])->group(function () { Route::get('/ssh', [SshController::class, 'index'])->name('dashboard.ssh'); Route::post('/ssh/command-details', [SshController::class, 'getCommandDetails'])->name('ssh.command-details'); Route::post('/ssh/execute', [SshController::class, 'executeCommand'])->name('ssh.execute'); });
Route definitions for the component.
Views (Optional - JSON)
{ "index.blade.php": "@extends('layouts.app')\n\n@section('title', 'SSH Commands - Matomo Tools')\n\n@section('styles')\n@include('dashboard._floating_container_styles')\n<style>\n.floating-container {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.command-btn.active {\n border-color: var(--accent-blue);\n box-shadow: 0 0 0 1px rgba(96,165,250,0.4);\n}\n\n.output-area {\n background-color: rgba(15, 23, 42, 0.6);\n color: #e2e8f0;\n font-family: 'Fira Code', monospace;\n font-size: 14px;\n padding: 1rem;\n border-radius: 12px;\n max-height: 420px;\n overflow-y: auto;\n white-space: pre-wrap;\n word-wrap: break-word;\n border: 1px solid rgba(148, 163, 184, 0.18);\n}\n\n.output-area.success {\n border-left: 4px solid var(--accent-emerald);\n}\n\n.output-area.error {\n border-left: 4px solid var(--accent-rose);\n}\n\n.loading {\n width: 18px;\n height: 18px;\n border: 3px solid rgba(148, 163, 184, 0.15);\n border-top-color: var(--accent-blue);\n border-radius: 50%;\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to { transform: rotate(360deg); }\n}\n<\/style>\n@endsection\n\n@section('content')\n<div class=\"floating-container\">\n <div class=\"row\">\n <!-- Header -->\n <div class=\"col-12 mb-4\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h5 class=\"card-title mb-0\">\n <i class=\"fas fa-terminal me-2\"><\/i>SSH Command Interface\n <\/h5>\n <\/div>\n <div class=\"card-body\">\n @if(!$sshConfigured)\n <div class=\"alert alert-warning\">\n <i class=\"fas fa-exclamation-triangle me-2\"><\/i>\n <strong>SSH not configured!<\/strong> Please configure your SSH settings in \n <a href=\"{{ route('profile.edit') }}\" class=\"alert-link\">Profile Settings<\/a> before using this interface.\n <\/div>\n @else\n <div class=\"alert alert-success\">\n <i class=\"fas fa-check-circle me-2\"><\/i>\n SSH configured for: <strong>{{ auth()->user()->ssh_host }}<\/strong>\n <\/div>\n @endif\n <\/div>\n <\/div>\n <\/div>\n\n <!-- Command Selection -->\n <div class=\"col-lg-4 mb-4\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h5 class=\"card-title mb-0\">Select a Command<\/h5>\n <\/div>\n <div class=\"card-body\" style=\"max-height: 600px; overflow-y: auto;\">\n @foreach($commandsByCategory as $category => $commands)\n <div class=\"mb-4\">\n <h6 class=\"text-muted text-uppercase mb-2\">\n <i class=\"fas fa-folder me-1\"><\/i>{{ $category }}\n <\/h6>\n <div class=\"d-grid gap-2\">\n @foreach($commands as $command)\n <button \n type=\"button\" \n class=\"btn btn-outline-primary btn-sm text-start command-btn\"\n data-command=\"{{ $command['name'] }}\"\n >\n <div class=\"fw-bold\">{{ $command['name'] }}<\/div>\n <small class=\"text-muted\">{{ \\Illuminate\\Support\\Str::limit($command['description'], 60) }}<\/small>\n <\/button>\n @endforeach\n <\/div>\n <\/div>\n @endforeach\n <\/div>\n <\/div>\n <\/div>\n\n <!-- Command Form and Output -->\n <div class=\"col-lg-8 mb-4\">\n <!-- Command Form -->\n <div class=\"card mb-4\" id=\"commandFormCard\" style=\"display: none;\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"card-title mb-0\" id=\"commandTitle\">Command Parameters<\/h5>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" id=\"clearCommandBtn\">\n <i class=\"fas fa-times\"><\/i> Clear\n <\/button>\n <\/div>\n <div class=\"card-body\">\n <div id=\"commandDescription\" class=\"mb-3 text-muted\"><\/div>\n <form id=\"commandForm\">\n <input type=\"hidden\" id=\"selectedCommand\" name=\"command\">\n <div id=\"commandParameters\"><\/div>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary\" id=\"executeBtn\" {{ !$sshConfigured ? 'disabled' : '' }}>\n <i class=\"fas fa-play me-2\"><\/i>Execute Command\n <\/button>\n <button type=\"button\" class=\"btn btn-secondary\" id=\"previewBtn\">\n <i class=\"fas fa-eye me-2\"><\/i>Preview Command\n <\/button>\n <\/div>\n <\/form>\n <\/div>\n <\/div>\n\n <!-- Command Preview -->\n <div class=\"card mb-4\" id=\"commandPreviewCard\" style=\"display: none;\">\n <div class=\"card-header\">\n <h5 class=\"card-title mb-0\">Command Preview<\/h5>\n <\/div>\n <div class=\"card-body\">\n <code id=\"commandPreview\" class=\"d-block p-3 bg-light border rounded\"><\/code>\n <\/div>\n <\/div>\n\n <!-- Output Area -->\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"card-title mb-0\">Output<\/h5>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" id=\"clearOutputBtn\">\n <i class=\"fas fa-eraser\"><\/i> Clear\n <\/button>\n <\/div>\n <div class=\"card-body p-0\">\n <div id=\"outputArea\" class=\"output-area\">\n <div class=\"text-muted p-3 text-center\">Select a command to get started...<\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n <\/div>\n<\/div>\n\n<script>\nlet currentCommand = null;\nlet commandData = null;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n document.querySelectorAll('.command-btn').forEach(button => {\n button.addEventListener('click', (event) => {\n const commandName = button.dataset.command;\n selectCommand(event, commandName);\n });\n });\n\n const executeBtn = document.getElementById('executeBtn');\n if (executeBtn) {\n executeBtn.addEventListener('click', executeCommand);\n }\n\n const previewBtn = document.getElementById('previewBtn');\n if (previewBtn) {\n previewBtn.addEventListener('click', (event) => {\n event.preventDefault();\n previewCommand();\n });\n }\n\n const clearCommandBtn = document.getElementById('clearCommandBtn');\n if (clearCommandBtn) {\n clearCommandBtn.addEventListener('click', (event) => {\n event.preventDefault();\n clearCommand();\n });\n }\n\n const clearOutputBtn = document.getElementById('clearOutputBtn');\n if (clearOutputBtn) {\n clearOutputBtn.addEventListener('click', (event) => {\n event.preventDefault();\n clearOutput();\n });\n }\n});\n\nasync function selectCommand(eventOrNull, commandName) {\n const evt = eventOrNull || window.event || null;\n\n \/\/ Update UI\n document.querySelectorAll('.command-btn').forEach(btn => {\n btn.classList.remove('active', 'btn-primary');\n btn.classList.add('btn-outline-primary');\n });\n\n let clickedButton = null;\n if (evt && evt.currentTarget) {\n clickedButton = evt.currentTarget;\n } else if (evt && evt.target && typeof evt.target.closest === 'function') {\n clickedButton = evt.target.closest('.command-btn');\n }\n if (!clickedButton) {\n clickedButton = document.querySelector(`.command-btn[data-command=\"${commandName}\"]`);\n }\n\n if (clickedButton) {\n clickedButton.classList.add('active', 'btn-primary');\n clickedButton.classList.remove('btn-outline-primary');\n }\n \n \/\/ Show loading\n document.getElementById('commandFormCard').style.display = 'block';\n document.getElementById('commandParameters').innerHTML = '<div class=\"text-center p-3\"><div class=\"loading\"><\/div> Loading command details...<\/div>';\n \n \/\/ Fetch command details\n try {\n const response = await fetch('{{ route(\"ssh.command-details\") }}', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/json',\n 'X-CSRF-TOKEN': '{{ csrf_token() }}'\n },\n body: JSON.stringify({ command: commandName })\n });\n\n const data = await handleJsonResponse(response);\n if (data.success) {\n currentCommand = commandName;\n commandData = data.command;\n renderCommandForm(data.command);\n } else {\n alert('Error loading command: ' + (data.message || 'Unknown error'));\n console.error('Command details error payload:', data);\n }\n } catch (error) {\n console.error('Error fetching command details:', error);\n alert(error.message || 'Error loading command details');\n document.getElementById('commandParameters').innerHTML = '<div class=\"text-danger\">Failed to load command details. Check console for more info.<\/div>';\n }\n}\n\nfunction renderCommandForm(command) {\n document.getElementById('selectedCommand').value = command.name;\n document.getElementById('commandTitle').textContent = command.name;\n document.getElementById('commandDescription').textContent = command.description;\n \n let html = '';\n const parameters = command.parameters || {};\n \n \/\/ Separate arguments and options\n const argumentsList = [];\n const optionsList = [];\n \n for (const [paramName, paramConfig] of Object.entries(parameters)) {\n if (paramConfig.is_argument) {\n argumentsList.push([paramName, paramConfig]);\n } else {\n optionsList.push([paramName, paramConfig]);\n }\n }\n \n \/\/ Render arguments section\n if (argumentsList.length > 0) {\n html += '<h6 class=\"mt-3 mb-2 text-muted\">Arguments<\/h6>';\n let argIndex = 0;\n for (const [paramName, paramConfig] of argumentsList) {\n html += renderParameterField(paramName, paramConfig, argIndex, true);\n argIndex++;\n }\n }\n \n \/\/ Render options section\n if (optionsList.length > 0) {\n html += '<h6 class=\"mt-3 mb-2 text-muted\">Options<\/h6>';\n for (const [paramName, paramConfig] of optionsList) {\n html += renderParameterField(paramName, paramConfig, null, false);\n }\n }\n \n if (html === '') {\n html = '<p class=\"text-muted\">This command has no parameters.<\/p>';\n }\n \n document.getElementById('commandParameters').innerHTML = html;\n}\n\nfunction renderParameterField(paramName, paramConfig, argIndex = null, isArgument = false) {\n \/\/ Use argument name if available, otherwise use index\n const fieldName = isArgument \n ? (argIndex !== null ? `args[${paramName}]` : `args[${argIndex}]`)\n : `opts[${paramName}]`;\n const isRequired = paramConfig.required || false;\n const paramType = paramConfig.type || 'string';\n const description = paramConfig.description || '';\n const options = paramConfig.options || null;\n const acceptValue = paramConfig.accept_value !== false; \/\/ Default to true if not specified\n \n \/\/ Truncate description to 100 characters\n const shortDescription = description.length > 100 ? description.substring(0, 100) + '...' : description;\n \n let html = '<div class=\"mb-3\">';\n html += `<label for=\"param_${paramName}\" class=\"form-label\">`;\n html += paramName.replace(\/-\/g, ' ').replace(\/\\b\\w\/g, l => l.toUpperCase());\n if (isRequired) html += ' <span class=\"text-danger\">*<\/span>';\n html += '<\/label>';\n \n \/\/ Check if it's a boolean option (accept_value: false) or type is boolean\n if (!acceptValue || paramType === 'boolean') {\n \/\/ Checkbox for boolean options\n html += `<div class=\"form-check\">`;\n html += `<input class=\"form-check-input\" type=\"checkbox\" id=\"param_${paramName}\" name=\"${fieldName}\" value=\"1\">`;\n html += `<label class=\"form-check-label\" for=\"param_${paramName}\">Enable ${paramName.replace(\/-\/g, ' ').replace(\/\\b\\w\/g, l => l.toUpperCase())}<\/label>`;\n html += `<\/div>`;\n } else if (options && Array.isArray(options) && options.length > 0 && options.length <= 20) {\n \/\/ Select dropdown - only if options array exists, has values, and has reasonable number of options (max 20)\n \/\/ If more than 20 options, it's likely a free text field (like comma-separated IDs)\n html += `<select class=\"form-select\" id=\"param_${paramName}\" name=\"${fieldName}\" ${isRequired ? 'required' : ''}>`;\n html += '<option value=\"\">Select...<\/option>';\n options.forEach(opt => {\n const optValue = typeof opt === 'object' ? opt.value || opt : opt;\n const optLabel = typeof opt === 'object' ? opt.label || opt.value || opt : opt;\n html += `<option value=\"${escapeHtml(optValue)}\">${escapeHtml(optLabel)}<\/option>`;\n });\n html += '<\/select>';\n } else if (paramType === 'integer' || paramType === 'number') {\n \/\/ Number input\n html += `<input type=\"number\" class=\"form-control\" id=\"param_${paramName}\" name=\"${fieldName}\" ${isRequired ? 'required' : ''} placeholder=\"${escapeHtml(shortDescription)}\">`;\n } else {\n \/\/ Text input - default for string types, comma-separated values, etc.\n html += `<input type=\"text\" class=\"form-control\" id=\"param_${paramName}\" name=\"${fieldName}\" ${isRequired ? 'required' : ''} placeholder=\"${escapeHtml(shortDescription)}\">`;\n }\n \n \/\/ Show truncated description as help text\n if (description) {\n html += `<small class=\"form-text text-muted d-block mt-1\">${escapeHtml(shortDescription)}<\/small>`;\n }\n \n html += '<\/div>';\n return html;\n}\n\nfunction previewCommand() {\n if (!currentCommand) return;\n \n const formData = new FormData(document.getElementById('commandForm'));\n const args = {};\n const opts = {};\n \n \/\/ Collect form data\n formData.forEach((value, key) => {\n if (key.startsWith('args[')) {\n \/\/ Handle both args[paramName] and args[0] format\n const match = key.match(\/args\\[(.+?)\\]\/);\n if (match) {\n const argKey = match[1];\n if (value) args[argKey] = value;\n }\n } else if (key.startsWith('opts[')) {\n const match = key.match(\/opts\\[(.+?)\\]\/);\n if (match) {\n const optKey = match[1];\n if (value && value !== '0' && value !== 'false') opts[optKey] = value;\n }\n }\n });\n \n \/\/ Build preview (simplified)\n let preview = `php console ${currentCommand}`;\n \/\/ Add arguments in order (by name or index)\n Object.values(args).forEach(arg => {\n if (arg) preview += ' ' + arg;\n });\n \/\/ Add options\n Object.entries(opts).forEach(([key, value]) => {\n if (value === '1' || value === true || value === 'true') {\n preview += ` --${key}`;\n } else if (value) {\n preview += ` --${key}=${value}`;\n }\n });\n \n document.getElementById('commandPreview').textContent = preview;\n document.getElementById('commandPreviewCard').style.display = 'block';\n}\n\nasync function executeCommand(event) {\n if (event) {\n event.preventDefault();\n }\n \n @if(!$sshConfigured)\n alert('SSH is not configured. Please configure SSH in your profile settings first.');\n return;\n @endif\n \n const formData = new FormData(document.getElementById('commandForm'));\n const args = {};\n const opts = {};\n \n \/\/ Collect form data\n formData.forEach((value, key) => {\n if (key.startsWith('args[')) {\n \/\/ Handle both args[paramName] and args[0] format\n const match = key.match(\/args\\[(.+?)\\]\/);\n if (match) {\n const argKey = match[1];\n if (value) args[argKey] = value;\n }\n } else if (key.startsWith('opts[')) {\n const match = key.match(\/opts\\[(.+?)\\]\/);\n if (match) {\n const optKey = match[1];\n if (value && value !== '0' && value !== 'false') opts[optKey] = value;\n }\n }\n });\n \n \/\/ Disable button and show loading\n const executeBtn = document.getElementById('executeBtn');\n executeBtn.disabled = true;\n executeBtn.innerHTML = '<div class=\"loading\"><\/div> Executing...';\n \n \/\/ Clear previous output\n const outputArea = document.getElementById('outputArea');\n outputArea.className = 'output-area';\n outputArea.innerHTML = '<div class=\"text-center p-3\"><div class=\"loading\"><\/div> Executing command...<\/div>';\n \n \/\/ Execute command\n try {\n const response = await fetch('{{ route(\"ssh.execute\") }}', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application\/json',\n 'X-CSRF-TOKEN': '{{ csrf_token() }}'\n },\n body: JSON.stringify({\n command: currentCommand,\n args: args,\n opts: opts\n })\n });\n\n const data = await handleJsonResponse(response);\n executeBtn.disabled = false;\n executeBtn.innerHTML = '<i class=\"fas fa-play me-2\"><\/i>Execute Command';\n \n if (data.success) {\n outputArea.className = 'output-area success';\n outputArea.innerHTML = `<div class=\"mb-2\"><strong>Command:<\/strong> <code>${escapeHtml(data.command)}<\/code><\/div><hr><div>${escapeHtml(data.output)}<\/div>`;\n } else {\n outputArea.className = 'output-area error';\n outputArea.innerHTML = `<div class=\"text-danger\"><strong>Error:<\/strong> ${escapeHtml(data.message || 'Unknown error')}<\/div>`;\n }\n } catch (error) {\n console.error('Error executing command:', error);\n executeBtn.disabled = false;\n executeBtn.innerHTML = '<i class=\"fas fa-play me-2\"><\/i>Execute Command';\n outputArea.className = 'output-area error';\n outputArea.innerHTML = `<div class=\"text-danger\"><strong>Error:<\/strong> ${escapeHtml(error.message || error)}<\/div>`;\n }\n}\n\nfunction clearCommand() {\n currentCommand = null;\n commandData = null;\n document.getElementById('commandFormCard').style.display = 'none';\n document.getElementById('commandPreviewCard').style.display = 'none';\n document.querySelectorAll('.command-btn').forEach(btn => {\n btn.classList.remove('active', 'btn-primary');\n btn.classList.add('btn-outline-primary');\n });\n}\n\nfunction clearOutput() {\n document.getElementById('outputArea').innerHTML = '<div class=\"text-muted p-3 text-center\">Output will appear here...<\/div>';\n document.getElementById('outputArea').className = 'output-area';\n}\n\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\nfunction handleJsonResponse(response) {\n const contentType = response.headers.get('content-type') || '';\n if (!response.ok) {\n return response.text().then(text => {\n const snippet = text.substring(0, 400);\n console.error('Non-OK response:', response.status, snippet);\n throw new Error(`Request failed (${response.status}). Check server logs. Snippet: ${snippet}`);\n });\n }\n if (!contentType.includes('application\/json')) {\n return response.text().then(text => {\n const snippet = text.substring(0, 400);\n console.error('Unexpected response format. Snippet:', snippet);\n throw new Error('Unexpected response format. Expected JSON but received: ' + snippet);\n });\n }\n return response.json();\n}\n<\/script>\n@endsection\n\n" }
JSON object with view file paths or inline content.
Assets (Optional - JSON)
[]
JSON array of asset file paths.
Update Component
Delete Component