Files
rspade_system/app/RSpade/Commands/Rsx/Route_Debug_Command.php
root 84ca3dfe42 Fix code quality violations and rename select input components
Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:39:43 +00:00

599 lines
27 KiB
PHP
Executable File

<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Commands\Rsx;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
use App\RSpade\Core\Debug\Debugger;
/**
* RSX Route Debug Command
* ========================
*
* PURPOSE:
* This is a comprehensive route debugging tool designed for the RSpade project.
* It uses Playwright to launch a real browser and fetch a route, capturing extensive
* diagnostic information about the request, response, JavaScript execution, and logs.
*
* HOW IT WORKS:
* 1. Rotates all development logs for clean slate debugging
* 2. Launches a headless Chromium browser via Playwright
* 3. Navigates to the specified route on localhost
* 4. Captures response, console output, XHR requests, DOM state, and more
* 5. Outputs results in a terse, log-like format for easy debugging
* 6. Rotates logs again after test for clean slate on next run
*
* KEY FEATURES:
* - Backdoor authentication: Use --user-id to bypass login and test as any user
* - Plain text error output: Errors returned as plain text with stack traces
* - Console capture: JavaScript errors and logs captured (--console for all)
* - XHR/fetch tracking: Monitor API calls with --xhr-dump or --xhr-list
* - Element verification: Check DOM elements with --expect-element
* - HTML extraction: Get element HTML with --dump-element
* - Storage inspection: View localStorage/sessionStorage with --storage
* - Form inspection: List all input elements with --input-elements
* - Cookie display: Show all cookies with --cookies
* - Redirect following: Track redirect chains with --follow-redirects
* - POST requests: Send JSON data with --post
* - Headers inspection: Display all headers with --headers
* - Log integration: Show Laravel/nginx logs with --log or --all-logs
* - Wait for elements: Delay capture with --wait-for selector
* - Full output mode: Enable all display options with --full
*
* DESIGN GOALS:
* - Data-driven debugging - Focus on data flow, not visual presentation
* - Minimal, terse output - No fancy formatting or colors in the output
* - Log-like format - Output designed to be easily parsed or grepped
* - Real browser testing - Tests the full stack including JavaScript
* - Development only - This tool is disabled in production environments
* - Clean slate testing - Logs rotated before/after each test
*
* USAGE EXAMPLES:
* php artisan rsx:debug /dashboard # Basic route test
* php artisan rsx:debug /dashboard --user-id=1 # Test as user ID 1
* php artisan rsx:debug /api/users --no-body # Headers only
* php artisan rsx:debug /login --full # Maximum information
* php artisan rsx:debug /api/data --xhr-list # Simple XHR list
* php artisan rsx:debug /api/data --xhr-dump # Full XHR details
* php artisan rsx:debug /form --expect-element="#submit" # Verify element exists
* php artisan rsx:debug /page --dump-element=".content" # Extract element HTML
* php artisan rsx:debug /app --storage # View storage data
* php artisan rsx:debug /form --input-elements # List form inputs
* php artisan rsx:debug /api --post='{"key":"value"}' # POST JSON data
* php artisan rsx:debug /slow --wait-for=".loaded" # Wait for element
* php artisan rsx:debug /auth --follow-redirects # Track redirects
* php artisan rsx:debug /page --all-logs # Show all log files
* php artisan rsx:debug /demo --eval="typeof jQuery" # Execute JavaScript code
*
* OMITTED FEATURES:
* This tool is designed for data-driven debugging, not visual testing or interaction.
* The following features are intentionally omitted as out of scope:
*
* - Screenshot capture: Not implemented as this tool focuses on data, not visuals
* - PDF generation: Not implemented as this tool is for debugging, not archival
* - Page interaction (click, fill, select): Out of scope for route debugging
* - Visual regression testing: Use dedicated visual testing tools instead
* - Performance profiling: Use browser DevTools or dedicated profiling tools
* - Accessibility testing: Use dedicated accessibility testing tools
*
* These omissions keep the tool focused on its core purpose: debugging data flow
* through routes, not testing UI appearance or user interactions.
*
* IMPLEMENTATION DETAILS:
* - Uses X-Playwright-Test header to trigger plain text error responses
* - Uses X-Dev-Auth-User-Id header for backdoor authentication
* - Auto-installs Playwright and Chromium if not present
* - Route interception prevents CORS issues with CDN resources
* - Works with both Laravel routes and RSX routes
* - Logs rotated via Debugger::logrotate() for clean testing
*
* SECURITY:
* - Only available in local/development/testing environments
* - Throws fatal error if attempted in production
* - Backdoor authentication only works in non-production environments
*
* OUTPUT FORMAT:
* The command outputs in a simple, parseable format:
* - Status line with route and response code
* - Redirect chain (if --follow-redirects used)
* - Response headers (if --headers used)
* - Console errors (always shown if present)
* - Console logs (if --console used)
* - XHR/fetch requests (if --xhr-dump or --xhr-list used)
* - Input elements (if --input-elements used)
* - Cookies (if --cookies used)
* - Storage data (if --storage used)
* - Element HTML (if --dump-element used)
* - Response body (unless --no-body used)
* - Laravel log errors (shown by default if errors exist)
* - Nginx error log (shown by default if errors exist)
* - All logs complete (if --all-logs used)
*
* The --full flag enables all display options except --no-body and --follow-redirects,
* providing maximum diagnostic information in a single command.
*/
class Route_Debug_Command extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'rsx:debug
{url? : The URL to debug (e.g., /dashboard, /api/users). Use --examples to see usage examples}
{--examples : Show comprehensive usage examples}
{--user= : Test as specific user ID (bypasses authentication)}
{--user-id= : Alias for --user option}
{--log : Display Laravel error log if not empty}
{--no-body : Suppress HTTP response body (show headers/status only)}
{--follow-redirects : Follow HTTP redirects and show full redirect chain}
{--headers : Display all HTTP response headers}
{--console : Display all browser console output (not just errors) and console_debug() output even if SHOW_CONSOLE_DEBUG_HTTP=false}
{--console-log : Alias for --console}
{--xhr-dump : Capture full details of XHR/fetch requests (URL, headers, body, response)}
{--input-elements : List all form input elements with values and attributes}
{--post= : Send POST request with JSON data (e.g., --post=\'{"key":"value"}\')}
{--cookies : Display all browser cookies with domains and expiry}
{--wait-for= : Wait for CSS selector before capture (e.g., --wait-for=".loaded")}
{--all-logs : Display Laravel log and nginx logs after test}
{--expect-element= : Verify element exists by CSS selector (fails if not found)}
{--dump-element= : Extract and display HTML of element by CSS selector}
{--storage : Display localStorage and sessionStorage contents}
{--xhr-list : Show simple list of XHR/fetch URLs and status codes}
{--full : Enable all display options except no-body and follow-redirects}
{--eval= : Execute JavaScript code in the page context and display the result}
{--timeout= : Navigation timeout in milliseconds (minimum 30000ms, default 30000ms)}
{--console-debug-filter= : Filter console_debug output to specific channel (e.g., BENCHMARK, DISPATCH)}
{--console-debug-benchmark : Include benchmark timing prefixes in console_debug output}
{--console-debug-all : Show all console_debug channels (overrides filter)}
{--console-debug-disable : Disable console_debug entirely for this test}
{--console-list : Alias for --console-log to display all console output}
{--screenshot-width= : Screenshot width (px or preset: mobile, iphone-mobile, tablet, desktop-small, desktop-medium, desktop-large). Defaults to 1920}
{--screenshot-path= : Path to save screenshot file (triggers screenshot capture, max height 5000px)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Debug URLs using headless browser - captures response, console, XHR, DOM state, storage, and logs (development only)';
/**
* Execute the console command.
*/
public function handle()
{
// Check environment - throw fatal error in production
if (app()->environment('production')) {
throw new \RuntimeException('FATAL: rsx:debug command is not available in production environment. This is a development-only debugging tool.');
}
// Check if --examples was requested
if ($this->option('examples')) {
$this->showExamples();
return 0;
}
// Get the URL to debug
$url = $this->argument('url');
// If no URL provided, show help
if (!$url) {
$this->error('No URL provided. Use --examples to see usage examples.');
return 1;
}
// Check if console_debug is disabled globally and user didn't override
$console_debug_enabled = config('rsx.console_debug.enabled', false) || env('CONSOLE_DEBUG_ENABLED') === 'true';
$console_debug_override = $this->option('console') ||
$this->option('console-log') ||
$this->option('console-list') ||
$this->option('console-debug-all') ||
$this->option('console-debug-filter') ||
$this->option('console-debug-benchmark') ||
env('CONSOLE_DEBUG_FILTER') ||
env('CONSOLE_DEBUG_BENCHMARK');
$console_debug_disabled = $this->option('console-debug-disable') || env('CONSOLE_DEBUG_ENABLED') === 'false';
// If console_debug is disabled and not overridden, show a single line message
if (!$console_debug_enabled && !$console_debug_override && !$console_debug_disabled) {
$this->line('console_debug is disabled. Run `php artisan rsx:man console_debug` for more information on its usage.');
// Don't return early - still run the test, just with the message
}
// Ensure URL starts with /
if (!str_starts_with($url, '/')) {
$url = '/' . $url;
}
// Get user ID from options
$user_id = $this->option('user-id') ?: $this->option('user');
// Get log flag
$show_log = $this->option('log');
// Get no-body flag
$no_body = $this->option('no-body');
// Get follow-redirects flag
$follow_redirects = $this->option('follow-redirects');
// Get headers flag
$headers = $this->option('headers');
// Get console flag (--console or --console-log alias)
$console_log = $this->option('console') || $this->option('console-log');
// Get xhr-dump flag
$xhr_dump = $this->option('xhr-dump');
// Get input-elements flag
$input_elements = $this->option('input-elements');
// Get POST data
$post_data = $this->option('post');
// Get cookies flag
$cookies = $this->option('cookies');
// Get wait-for selector
$wait_for = $this->option('wait-for');
// Get all-logs flag
$all_logs = $this->option('all-logs');
// Get new feature flags
$expect_element = $this->option('expect-element');
$dump_element = $this->option('dump-element');
$storage = $this->option('storage');
$xhr_list = $this->option('xhr-list');
$full = $this->option('full');
$eval_code = $this->option('eval');
// Get timeout option and validate
$timeout = $this->option('timeout');
if ($timeout !== null) {
$timeout = intval($timeout);
if ($timeout < 30000) {
$this->error('❌ Timeout value is in milliseconds and must be no less than 30000 milliseconds (30 seconds)');
return 1;
}
} else {
$timeout = 30000; // Default 30 seconds
}
// Get screenshot options
$screenshot_width = $this->option('screenshot-width');
$screenshot_path = $this->option('screenshot-path');
// Get console debug options (with environment variable fallbacks)
$console_debug_filter = $this->option('console-debug-filter') ?: env('CONSOLE_DEBUG_FILTER');
$console_debug_benchmark = $this->option('console-debug-benchmark') ?: env('CONSOLE_DEBUG_BENCHMARK', false);
$console_debug_all = $this->option('console-debug-all') ?: (env('CONSOLE_DEBUG_FILTER') === 'ALL');
$console_debug_disable = $this->option('console-debug-disable') ?: (env('CONSOLE_DEBUG_ENABLED') === 'false');
// Auto-enable console-log and console_debug when console-debug-filter is set
if ($console_debug_filter && !$console_debug_disable) {
$console_log = true; // Enable console log output
// console_debug is enabled via the filter itself
}
// Auto-enable console_debug when CONSOLE_DEBUG_ENABLED=true
if (env('CONSOLE_DEBUG_ENABLED') === 'true' && !$console_debug_disable) {
// Enable console output if not explicitly disabled
if (!$console_debug_filter && !$console_debug_all) {
$console_debug_all = true; // Show all channels if no specific filter
}
}
$console_list = $this->option('console-list');
// console-list is an alias for console-log
if ($console_list) {
$console_log = true;
}
// Rotate logs before test to ensure clean slate
Debugger::logrotate();
// Check if Playwright script exists
$playwright_script = base_path('bin/route-debug.js');
if (!file_exists($playwright_script)) {
$this->error("❌ Playwright script not found: {$playwright_script}");
$this->error('Please create the script or check your installation');
return 1;
}
// Check if node/npm is available
$node_check = new Process(['node', '--version']);
$node_check->run();
if (!$node_check->isSuccessful()) {
$this->error('❌ Node.js is not installed or not in PATH');
return 1;
}
// Check if playwright is installed
$playwright_check = new Process(['node', '-e', "require('playwright')"], base_path());
$playwright_check->run();
if (!$playwright_check->isSuccessful()) {
$this->warn('⚠️ Playwright not installed. Installing now...');
$npm_install = new Process(['npm', 'install', 'playwright'], base_path());
$npm_install->run(function ($type, $buffer) {
echo $buffer;
});
if (!$npm_install->isSuccessful()) {
$this->error('❌ Failed to install Playwright');
return 1;
}
$this->info('✅ Playwright installed');
$this->info('');
}
// Check if chromium browser is installed and up to date
$browser_check_script = "const {chromium} = require('playwright'); chromium.launch({headless:true}).then(b => {b.close(); process.exit(0);}).catch(e => {console.error(e.message); process.exit(1);});";
$browser_check = new Process(['node', '-e', $browser_check_script], base_path(), $_ENV, null, 10);
$browser_check->run();
if (!$browser_check->isSuccessful()) {
$error_output = $browser_check->getErrorOutput() . $browser_check->getOutput();
// Check if it's a browser not installed or out of date error
if (str_contains($error_output, "Executable doesn't exist") ||
str_contains($error_output, "browserType.launch") ||
str_contains($error_output, "Playwright was just installed or updated")) {
$this->info('Installing/updating Chromium browser...');
$browser_install = new Process(['npx', 'playwright', 'install', 'chromium'], base_path());
$browser_install->setTimeout(300); // 5 minute timeout for download
$browser_install->run(function ($type, $buffer) {
// Silent - downloads can be verbose
});
if (!$browser_install->isSuccessful()) {
$this->error('❌ Failed to install Chromium browser');
$this->error('Run manually: npx playwright install chromium');
return 1;
}
$this->info('✅ Chromium browser installed/updated');
$this->info('');
} else {
$this->error('❌ Browser check failed: ' . trim($error_output));
return 1;
}
}
// Build command arguments
$command_args = ['node', $playwright_script, $url];
if ($user_id) {
$command_args[] = "--user-id={$user_id}";
}
if ($show_log) {
$command_args[] = '--log';
}
if ($no_body) {
$command_args[] = '--no-body';
}
if ($follow_redirects) {
$command_args[] = '--follow-redirects';
}
if ($headers) {
$command_args[] = '--headers';
}
if ($console_log) {
$command_args[] = '--console-log';
}
if ($xhr_dump) {
$command_args[] = '--xhr-dump';
}
if ($input_elements) {
$command_args[] = '--input-elements';
}
if ($post_data) {
$command_args[] = "--post={$post_data}";
}
if ($cookies) {
$command_args[] = '--cookies';
}
if ($wait_for) {
$command_args[] = "--wait-for={$wait_for}";
}
if ($all_logs) {
$command_args[] = '--all-logs';
}
if ($expect_element) {
$command_args[] = "--expect-element={$expect_element}";
}
if ($dump_element) {
$command_args[] = "--dump-element={$dump_element}";
}
if ($storage) {
$command_args[] = '--storage';
}
if ($xhr_list) {
$command_args[] = '--xhr-list';
}
if ($full) {
$command_args[] = '--full';
}
if ($eval_code) {
// Don't use escapeshellarg here as it adds extra quotes
// Just pass the eval code directly since Process class handles escaping
$command_args[] = "--eval={$eval_code}";
}
if ($timeout) {
$command_args[] = "--timeout={$timeout}";
}
if ($console_debug_filter) {
$command_args[] = "--console-debug-filter={$console_debug_filter}";
}
if ($console_debug_benchmark) {
$command_args[] = "--console-debug-benchmark";
}
if ($console_debug_all) {
$command_args[] = "--console-debug-all";
}
if ($console_debug_disable) {
$command_args[] = "--console-debug-disable";
}
if ($screenshot_width) {
$command_args[] = "--screenshot-width={$screenshot_width}";
}
if ($screenshot_path) {
$command_args[] = "--screenshot-path={$screenshot_path}";
}
// Pass Laravel log path as environment variable
$laravel_log_path = storage_path('logs/laravel.log');
$env = array_merge($_ENV, [
'LARAVEL_LOG_PATH' => $laravel_log_path
]);
// Add console debug filter to environment if provided
if ($console_debug_filter) {
$env['CONSOLE_DEBUG_FILTER'] = $console_debug_filter;
$env['CONSOLE_DEBUG_ENABLED'] = 'true'; // Enable console_debug when filter is set
}
// Convert timeout from milliseconds to seconds for Process timeout
// Add 10 seconds buffer to the Process timeout to allow Playwright to timeout first
$process_timeout = ($timeout / 1000) + 10;
// Release the application lock before running Playwright to prevent lock contention
// The artisan command holds a WRITE lock which would block the web request's READ lock
\App\RSpade\Core\Bootstrap\RsxBootstrap::temporarily_release_lock();
$process = new Process(
$command_args,
base_path(),
$env,
null,
$process_timeout
);
$process->run(function ($type, $buffer) {
// Output directly to console
echo $buffer;
});
// Rotate logs after test to clean slate for next run
Debugger::logrotate();
return $process->isSuccessful() ? 0 : 1;
}
/**
* Show comprehensive usage examples
*/
protected function showExamples()
{
$this->info('RSX Debug Command - Comprehensive Usage Examples');
$this->line('=================================================');
$this->line('');
$this->comment('BASIC USAGE:');
$this->line(' php artisan rsx:debug /dashboard # Test a URL');
$this->line(' php artisan rsx:debug /api/users --no-body # Headers only');
$this->line(' php artisan rsx:debug /login --full # All information');
$this->line('');
$this->comment('AUTHENTICATION:');
$this->line(' php artisan rsx:debug /admin --user=1 # Test as user ID 1');
$this->line(' php artisan rsx:debug /profile --user-id=5 # Alternative syntax');
$this->line('');
$this->comment('TESTING RSX JAVASCRIPT:');
$this->line(' php artisan rsx:debug /demo --eval="Rsx.Route(\'Demo_Controller\').url()" --no-body');
$this->line(' php artisan rsx:debug /demo --eval="JSON.stringify(Rsx._routes)" --no-body');
$this->line(' php artisan rsx:debug /demo --eval="Rsx.is_dev()" --no-body');
$this->line('');
$this->comment('DEBUGGING OUTPUT:');
$this->line(' php artisan rsx:debug / --console # All console output');
$this->line(' php artisan rsx:debug / --console-log # Alias for --console');
$this->line(' php artisan rsx:debug / --console-debug-filter=AUTH # Filter console_debug');
$this->line(' php artisan rsx:debug / --console-debug-all # Show all console_debug channels');
$this->line(' php artisan rsx:debug / --console-debug-benchmark # With timing');
$this->line(' php artisan rsx:debug / --console-debug-disable # Disable console_debug');
$this->line(' php artisan rsx:debug / --log # Display Laravel error log');
$this->line(' php artisan rsx:debug / --all-logs # Show all log files');
$this->line('');
$this->comment('XHR/AJAX MONITORING:');
$this->line(' php artisan rsx:debug /api --xhr-list # Simple XHR list');
$this->line(' php artisan rsx:debug /api --xhr-dump # Full XHR details');
$this->line('');
$this->comment('DOM INSPECTION:');
$this->line(' php artisan rsx:debug /form --expect-element="#submit" # Verify element exists');
$this->line(' php artisan rsx:debug /page --dump-element=".content" # Extract element HTML');
$this->line(' php artisan rsx:debug /form --input-elements # List form inputs');
$this->line(' php artisan rsx:debug /slow --wait-for=".loaded" # Wait for element');
$this->line('');
$this->comment('HTTP TESTING:');
$this->line(' php artisan rsx:debug /api --post=\'{"key":"value"}\' # POST JSON data');
$this->line(' php artisan rsx:debug /auth --follow-redirects # Track redirects');
$this->line(' php artisan rsx:debug /api --headers # Display all headers');
$this->line(' php artisan rsx:debug /app --cookies # Show all cookies');
$this->line('');
$this->comment('BROWSER STATE:');
$this->line(' php artisan rsx:debug /app --storage # View localStorage/sessionStorage');
$this->line(' php artisan rsx:debug /slow --timeout=60000 # 60 second timeout');
$this->line('');
$this->comment('SCREENSHOTS:');
$this->line(' php artisan rsx:debug /page --screenshot-path=/tmp/screenshot.png');
$this->line(' # Screenshot at 1920px (default)');
$this->line(' php artisan rsx:debug /page --screenshot-width=mobile --screenshot-path=/tmp/mobile.png');
$this->line(' # Mobile device (412px)');
$this->line(' php artisan rsx:debug /page --screenshot-width=tablet --screenshot-path=/tmp/tablet.png');
$this->line(' # Tablet device (768px)');
$this->line(' php artisan rsx:debug /page --screenshot-width=1024 --screenshot-path=/tmp/custom.png');
$this->line(' # Custom width (1024px)');
$this->line(' # Available presets: mobile (412px), iphone-mobile (390px), tablet (768px),');
$this->line(' # desktop-small (1366px), desktop-medium (1920px), desktop-large (2560px)');
$this->line('');
$this->comment('IMPORTANT NOTES:');
$this->line(' • When using rsx:debug with grep and no output appears, re-run without grep');
$this->line(' to see the full context and any errors that may have occurred');
$this->line(' • Use rsx_dump_die() in your code for temporary debugging output');
$this->line(' • This command is development-only and disabled in production');
$this->line(' • For more details on console_debug: php artisan rsx:man console_debug');
$this->line(' • For config options: php artisan rsx:man config_rsx');
}
}