diff --git a/Logo.png b/Logo.png
new file mode 100644
index 0000000..22e4ac3
Binary files /dev/null and b/Logo.png differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5202759
--- /dev/null
+++ b/README.md
@@ -0,0 +1,390 @@
+# Website Stress Testing Tool - Enhanced Edition
+
+A professional stress testing application that simulates realistic traffic patterns to test production websites with support for 1-5000 concurrent users. Now with **Website Crawler** for realistic user simulation!
+
+## š Features
+
+### Core Testing
+- **Concurrent Users**: Simulate 1-5000 users simultaneously
+- **Traffic Patterns**: Steady, Burst, Ramp-Up, and Random patterns
+- **š·ļø Website Crawler**: NEW! Randomly navigate through website links like real users
+- **Real-Time Monitoring**: Live statistics and interactive charts
+- **CORS Proxy**: Bypass browser CORS restrictions with user agent rotation
+
+### Advanced Metrics
+- **Percentile Analysis**: P50, P95, and P99 response times
+- **Error Categorization**: Track 4xx, 5xx, timeout, and network errors separately
+- **Bandwidth Tracking**: Monitor total data sent and received
+- **Request History**: Live log of last 100 requests with filtering
+
+### User Experience
+- **š Theme Toggle**: Switch between dark and light modes
+- **ā” Test Presets**: Quick-load configurations (Light, Medium, Heavy, Spike)
+- **š¾ Save Configurations**: Save and reload custom test setups
+- **āØļø Keyboard Shortcuts**: S=Start, P=Pause, X=Stop
+- **š± Mobile Responsive**: Works great on all devices
+- **Export Results**: JSON and CSV export functionality
+
+### Premium UI
+- Modern dark/light theme with glassmorphism effects
+- Real-time charts with Chart.js
+- Smooth animations and transitions
+- Professional color-coded statistics
+
+## š Prerequisites
+
+- **Node.js** (v14 or higher) - Required for the CORS proxy server
+- **Modern Web Browser** (Chrome, Firefox, Edge, Safari)
+
+## š ļø Setup Instructions
+
+### Step 1: Install Node.js
+
+If you don't have Node.js installed:
+
+**Windows:**
+```powershell
+# Using Chocolatey
+choco install nodejs
+
+# Or download from: https://nodejs.org/
+```
+
+**macOS:**
+```bash
+# Using Homebrew
+brew install node
+```
+
+**Linux:**
+```bash
+# Ubuntu/Debian
+sudo apt install nodejs npm
+
+# Fedora
+sudo dnf install nodejs
+```
+
+### Step 2: Start the CORS Proxy Server
+
+The proxy server is required to bypass CORS restrictions when testing production websites.
+
+```powershell
+# Navigate to the project directory
+cd "C:\Users\DM\OneDrive - BCT\BCT-YT - Documents\Project-Files\HomeLoan\HTML\StressTest"
+
+# Start the proxy server
+node proxy-server.js
+```
+
+You should see:
+```
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+ā CORS Proxy Server for Stress Testing Tool ā
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+ā
Server running on: http://localhost:3000
+ā
Max connections: 5000
+ā
Request timeout: 30000ms
+```
+
+**Keep this terminal window open** while using the stress testing tool.
+
+### Step 3: Open the Stress Testing Tool
+
+Open `index.html` in your web browser:
+
+**Option A: Double-click** the `index.html` file
+
+**Option B: Use a local web server** (recommended)
+```powershell
+# In a NEW terminal window (keep proxy running in the first)
+python -m http.server 8080
+
+# Then open: http://localhost:8080
+```
+
+## š Usage Guide
+
+### Quick Start with Presets
+
+1. **Select a Preset**: Choose from Light, Medium, Heavy, or Spike test
+2. **Enter Target URL**: `https://example.com`
+3. **Click "Start Test"**: Watch real-time metrics
+4. **Review Results**: Scroll down after test completes
+
+### Basic Test
+
+1. **Enter Target URL**: `https://beyondcloud.solutions/tag/guides/`
+2. **Set Concurrent Users**: Use the slider (1-5000)
+3. **Set Duration**: Test duration in seconds (10-600)
+4. **Select Traffic Pattern**:
+ - **Steady**: Constant requests per second
+ - **Burst**: Traffic spikes at intervals
+ - **Ramp-Up**: Gradual increase in load
+ - **Random**: Realistic user behavior
+5. **Click "Start Test"**
+6. **Monitor Live Statistics**: Watch real-time metrics and charts
+7. **Stop or Wait**: Click "Stop" or let it complete
+8. **Review Results**: Scroll down to see detailed metrics
+9. **Export** (optional): Download results as JSON or CSV
+
+### š·ļø Crawler Mode (NEW!)
+
+Simulate real users by randomly navigating through website links:
+
+1. **Enable Crawler Mode**: Check the "Enable Crawler Mode" checkbox
+2. **Set Crawl Depth**: How many page hops per user (1-5)
+3. **Set Links Per Page**: Maximum links to extract (1-50)
+4. **Start Test**: Users will randomly click links and navigate the site
+5. **Monitor**: Watch the Request History to see different URLs being visited
+
+**How it works:**
+- Extracts links from HTML responses
+- Randomly selects links from the same domain
+- Simulates realistic browsing behavior
+- Tracks unique URLs visited
+
+### Advanced Options
+
+Click "Advanced Options" to configure:
+
+- **HTTP Method**: GET, POST, PUT, DELETE, PATCH
+- **Custom Headers**: Add authentication, content-type, etc.
+ ```json
+ {
+ "Authorization": "Bearer your-token",
+ "Content-Type": "application/json"
+ }
+ ```
+- **Request Body**: For POST/PUT requests
+ ```json
+ {
+ "key": "value"
+ }
+ ```
+- **Think Time**: Delay between requests (0-5000ms)
+
+### Keyboard Shortcuts
+
+- **S**: Start test
+- **P**: Pause/Resume test
+- **X**: Stop test
+
+### Theme Toggle
+
+Click the **š Theme** button in the header to switch between dark and light modes. Your preference is saved automatically.
+
+### Save & Load Configurations
+
+1. **Configure your test** with desired settings
+2. **Click "š¾ Save Current Config"**
+3. **Enter a name** for your configuration
+4. **Reload page** to see it in the presets dropdown
+5. **Select saved config** to quickly load those settings
+
+## š Understanding Results
+
+### Key Metrics
+
+- **Total Requests**: Number of HTTP requests sent
+- **Success Rate**: Percentage of successful responses (2xx, 3xx)
+- **Requests per Second (RPS)**: Average throughput
+- **Response Time**: Min, Max, Average, P50, P95, P99
+- **Failed Requests**: Number of errors or timeouts
+- **Bandwidth**: Total data sent and received
+
+### Percentiles (NEW!)
+
+- **P50 (Median)**: 50% of requests were faster than this
+- **P95**: 95% of requests were faster than this (good for SLAs)
+- **P99**: 99% of requests were faster than this (catches outliers)
+
+### Error Breakdown (NEW!)
+
+- **4xx Errors**: Client errors (bad request, not found, etc.)
+- **5xx Errors**: Server errors (internal server error, etc.)
+- **Timeout Errors**: Requests that exceeded timeout limit
+- **Network Errors**: Connection failures, DNS errors, etc.
+
+### Charts
+
+- **RPS Chart**: Shows request rate over time
+- **Response Time Chart**: Shows latency trends
+- **User Load vs Error Rate**: Correlates user count with error percentage
+
+### Request History (NEW!)
+
+- Live table showing last 100 requests
+- Color-coded by success/failure
+- Shows URL, status code, and response time
+- Auto-scrolls with new requests
+
+## š§ Configuration
+
+### Changing the Proxy URL
+
+If you deploy the proxy server to a different location, update the proxy URL in `script.js`:
+
+```javascript
+this.config = {
+ // ... other config
+ proxyUrl: 'http://your-server:3000' // Change this
+};
+```
+
+### Deploying to Production
+
+When hosting on a web server:
+
+1. **Update proxy server** `allowedOrigins` in `proxy-server.js`:
+ ```javascript
+ const CONFIG = {
+ allowedOrigins: 'https://your-domain.com', // Not '*'
+ };
+ ```
+
+2. **Deploy both files**:
+ - Frontend: `index.html`, `styles.css`, `script.js`
+ - Backend: `proxy-server.js`, `package.json`
+
+3. **Run proxy server** on your hosting:
+ ```bash
+ npm start
+ # or
+ node proxy-server.js
+ ```
+
+## šÆ Testing Your Website
+
+### Example: Testing with Crawler Mode
+
+1. Start proxy server: `node proxy-server.js`
+2. Open `index.html` in browser
+3. Enter URL: `https://beyondcloud.solutions`
+4. Enable Crawler Mode ā
+5. Set crawl depth: 2
+6. Set users: 50
+7. Set duration: 60 seconds
+8. Select pattern: Random
+9. Click "Start Test"
+10. Watch Request History to see different pages being visited
+11. Monitor results
+
+## ā ļø Important Notes
+
+### Browser Limitations
+
+- This tool runs in the browser, which has connection limits
+- For very high loads (1000+ users), consider server-side tools:
+ - Apache JMeter
+ - k6
+ - Artillery
+ - Gatling
+
+### CORS Proxy Security
+
+- **Development**: The proxy allows all origins (`*`)
+- **Production**: Update `allowedOrigins` to your specific domain
+- **Never** expose an open proxy to the internet
+
+### Responsible Testing
+
+- **Only test websites you own** or have permission to test
+- **Start with low user counts** to avoid overwhelming servers
+- **Monitor your target server** during tests
+- **Be aware** that aggressive testing may trigger rate limiting or security measures
+- **Crawler mode** generates more requests - use responsibly
+
+## š Troubleshooting
+
+### "All requests are failing"
+
+**Problem**: CORS proxy not running
+
+**Solution**: Start the proxy server:
+```powershell
+node proxy-server.js
+```
+
+### "Connection refused" errors
+
+**Problem**: Proxy server not accessible
+
+**Solutions**:
+1. Check proxy is running on port 3000
+2. Verify firewall isn't blocking port 3000
+3. Check `proxyUrl` in `script.js` matches your setup
+
+### "Request timeout" errors
+
+**Problem**: Target server is slow or unreachable
+
+**Solutions**:
+1. Increase timeout in `proxy-server.js` (line 16)
+2. Reduce concurrent users
+3. Increase think time
+4. Check target URL is accessible
+
+### Crawler not finding links
+
+**Problem**: Website uses JavaScript to render links
+
+**Solutions**:
+1. Crawler only works with server-rendered HTML
+2. Try disabling crawler mode for JavaScript-heavy sites
+3. Consider using headless browser tools for SPA testing
+
+## š File Structure
+
+```
+StressTest/
+āāā index.html # Main application UI (enhanced)
+āāā styles.css # Premium design system (dark/light theme)
+āāā script.js # Frontend logic (with crawler)
+āāā proxy-server.js # CORS proxy backend (enhanced)
+āāā package.json # Node.js configuration
+āāā README.md # This file
+```
+
+## š What's New in Enhanced Edition
+
+### Version 2.0 Features
+
+⨠**Website Crawler**: Simulate real user navigation
+š **Percentile Metrics**: P50, P95, P99 response times
+š **Error Categorization**: Detailed error breakdown
+š **Bandwidth Tracking**: Monitor data usage
+š **Request History**: Live log of recent requests
+ā” **Test Presets**: Quick-load common scenarios
+š¾ **Save Configurations**: Persist custom setups
+š **Theme Toggle**: Dark and light modes
+āØļø **Keyboard Shortcuts**: Quick controls
+šØ **Enhanced UI**: Improved mobile responsiveness
+š **User Agent Rotation**: More realistic traffic simulation
+
+## š Security Considerations
+
+1. **Proxy Server**: Restrict `allowedOrigins` in production
+2. **Rate Limiting**: Consider adding rate limits to the proxy
+3. **Authentication**: Add auth if exposing publicly
+4. **Monitoring**: Log requests for security auditing
+5. **Crawler Mode**: Be mindful of the additional load it generates
+
+## š License
+
+MIT License - Feel free to modify and use as needed.
+
+## š¤ Support
+
+For issues or questions:
+1. Check this README
+2. Review the browser console for errors
+3. Check proxy server logs
+4. Verify target website is accessible
+
+---
+
+**Happy Stress Testing! š**
+
+*Enhanced with Website Crawler & Advanced Metrics*
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..8e74278
--- /dev/null
+++ b/index.html
@@ -0,0 +1,396 @@
+
+
+
+
+
+
+
+ Beyond Cloud Technology - Website Stress Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select a preset...
+ š¢ Light Load (10 users, 30s)
+ š” Medium Load (100 users, 60s)
+ š“ Heavy Load (500 users, 120s)
+ š„ Spike Test (200 users, burst)
+
+ š¾ Save Current Config
+
+
+ Keyboard Shortcuts: S = Start | P = Pause/Resume | X = Stop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Idle
+
+
+
+
+
+
+
+ ā¶ļø Start Test
+ āøļø Pause
+ ā¹ļø Stop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Avg Response Time
+
0ms
+
+
+
+
+
+
Response Time Percentiles
+
+
+
+
+
+
+
+
+
Bandwidth Usage
+
+
Total Bandwidth
+
0 B
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Time
+ URL
+ Status
+ Response Time
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ š¾ Export as JSON
+ š Export as CSV
+
+
+
+
+
+ Metric
+ Value
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7002a11
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "stress-testing-tool",
+ "version": "1.0.0",
+ "description": "Website stress testing tool with CORS proxy",
+ "main": "proxy-server.js",
+ "scripts": {
+ "start": "node proxy-server.js",
+ "proxy": "node proxy-server.js"
+ },
+ "keywords": [
+ "stress-testing",
+ "load-testing",
+ "cors-proxy"
+ ],
+ "author": "",
+ "license": "MIT"
+}
diff --git a/proxy-server.js b/proxy-server.js
new file mode 100644
index 0000000..d6bf0f4
--- /dev/null
+++ b/proxy-server.js
@@ -0,0 +1,242 @@
+// ===================================
+// CORS PROXY SERVER
+// ===================================
+// This proxy server allows the stress testing tool to test
+// production websites without CORS restrictions.
+
+const http = require('http');
+const https = require('https');
+const url = require('url');
+
+const PORT = 3000;
+
+// Configuration
+const CONFIG = {
+ // Maximum request timeout (30 seconds)
+ timeout: 30000,
+
+ // Allowed origins (restrict to your stress testing tool's domain)
+ // Use '*' for development, specific domain for production
+ allowedOrigins: '*',
+
+ // Maximum concurrent connections
+ maxConnections: 5000,
+
+ // User agents for rotation
+ userAgents: [
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
+ ]
+};
+
+// Get random user agent
+function getRandomUserAgent() {
+ return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)];
+}
+
+// Create the proxy server
+const server = http.createServer((req, res) => {
+ // Handle CORS preflight requests
+ if (req.method === 'OPTIONS') {
+ handleCORS(res);
+ res.writeHead(200);
+ res.end();
+ return;
+ }
+
+ // Only allow POST requests to the proxy
+ if (req.method !== 'POST') {
+ res.writeHead(405, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
+ return;
+ }
+
+ // Parse request body
+ let body = '';
+ req.on('data', chunk => {
+ body += chunk.toString();
+ });
+
+ req.on('end', () => {
+ try {
+ const proxyRequest = JSON.parse(body);
+ handleProxyRequest(proxyRequest, res);
+ } catch (error) {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ error: 'Invalid JSON',
+ message: error.message
+ }));
+ }
+ });
+});
+
+// Handle the actual proxy request
+function handleProxyRequest(proxyRequest, clientRes) {
+ const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest;
+
+ // Validate target URL
+ if (!targetUrl) {
+ clientRes.writeHead(400, { 'Content-Type': 'application/json' });
+ clientRes.end(JSON.stringify({ error: 'targetUrl is required' }));
+ return;
+ }
+
+ let parsedUrl;
+ try {
+ parsedUrl = new URL(targetUrl);
+ } catch (error) {
+ clientRes.writeHead(400, { 'Content-Type': 'application/json' });
+ clientRes.end(JSON.stringify({ error: 'Invalid URL' }));
+ return;
+ }
+
+ // Determine if we need http or https
+ const protocol = parsedUrl.protocol === 'https:' ? https : http;
+
+ // Prepare request options with random user agent
+ const options = {
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port,
+ path: parsedUrl.pathname + parsedUrl.search,
+ method: method,
+ headers: {
+ ...headers,
+ 'User-Agent': getRandomUserAgent(),
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
+ 'Accept-Language': 'en-US,en;q=0.5',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'DNT': '1',
+ 'Connection': 'keep-alive',
+ 'Upgrade-Insecure-Requests': '1'
+ },
+ timeout: CONFIG.timeout
+ };
+
+ const startTime = Date.now();
+
+ // Make the request to the target server
+ const proxyReq = protocol.request(options, (proxyRes) => {
+ const responseTime = Date.now() - startTime;
+
+ // Collect response data
+ let responseData = '';
+ let responseSize = 0;
+ const maxBodySize = 500000; // 500KB limit for crawler
+
+ proxyRes.on('data', chunk => {
+ responseSize += chunk.length;
+ // Only collect body if under size limit (for crawler)
+ if (responseSize < maxBodySize) {
+ responseData += chunk.toString();
+ }
+ });
+
+ proxyRes.on('end', () => {
+ // Send response back to client with CORS headers
+ handleCORS(clientRes);
+ clientRes.writeHead(200, { 'Content-Type': 'application/json' });
+
+ clientRes.end(JSON.stringify({
+ success: true,
+ statusCode: proxyRes.statusCode,
+ statusMessage: proxyRes.statusMessage,
+ responseTime: responseTime,
+ headers: proxyRes.headers,
+ body: responseData, // Full body for crawler link extraction
+ bodySize: responseSize
+ }));
+ });
+ });
+
+ // Handle request errors
+ proxyReq.on('error', (error) => {
+ const responseTime = Date.now() - startTime;
+
+ handleCORS(clientRes);
+ clientRes.writeHead(200, { 'Content-Type': 'application/json' });
+
+ clientRes.end(JSON.stringify({
+ success: false,
+ error: error.message,
+ responseTime: responseTime,
+ statusCode: 0
+ }));
+ });
+
+ // Handle timeout
+ proxyReq.on('timeout', () => {
+ proxyReq.destroy();
+ const responseTime = Date.now() - startTime;
+
+ handleCORS(clientRes);
+ clientRes.writeHead(200, { 'Content-Type': 'application/json' });
+
+ clientRes.end(JSON.stringify({
+ success: false,
+ error: 'Request timeout',
+ responseTime: responseTime,
+ statusCode: 0
+ }));
+ });
+
+ // Send request body if present
+ if (body && method !== 'GET' && method !== 'HEAD') {
+ proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body));
+ }
+
+ proxyReq.end();
+}
+
+// Add CORS headers to response
+function handleCORS(res) {
+ res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins);
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+}
+
+// Start the server
+server.listen(PORT, () => {
+ console.log(`
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+ā CORS Proxy Server for Stress Testing Tool ā
+āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
+
+ā
Server running on: http://localhost:${PORT}
+ā
Max connections: ${CONFIG.maxConnections}
+ā
Request timeout: ${CONFIG.timeout}ms
+
+š Usage:
+ POST to http://localhost:${PORT} with JSON body:
+ {
+ "targetUrl": "https://example.com",
+ "method": "GET",
+ "headers": {},
+ "body": null
+ }
+
+š Security Note:
+ For production, update CONFIG.allowedOrigins to your
+ stress testing tool's domain (not '*')
+
+Press Ctrl+C to stop the server
+ `);
+});
+
+// Handle server errors
+server.on('error', (error) => {
+ console.error('ā Server error:', error.message);
+ process.exit(1);
+});
+
+// Graceful shutdown
+process.on('SIGINT', () => {
+ console.log('\n\nš Shutting down proxy server...');
+ server.close(() => {
+ console.log('ā
Server closed');
+ process.exit(0);
+ });
+});
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..1db5049
--- /dev/null
+++ b/script.js
@@ -0,0 +1,1329 @@
+// ===================================
+// STRESS TESTING TOOL - MAIN SCRIPT
+// Enhanced with Crawler & Advanced Features
+// ===================================
+
+// ===================================
+// WEBSITE CRAWLER CLASS
+// ===================================
+class WebsiteCrawler {
+ constructor() {
+ this.visitedUrls = new Set();
+ this.urlQueue = [];
+ }
+
+ extractLinks(html, baseUrl) {
+ const links = [];
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const anchorTags = doc.querySelectorAll('a[href]');
+
+ const baseUrlObj = new URL(baseUrl);
+
+ anchorTags.forEach(anchor => {
+ try {
+ const href = anchor.getAttribute('href');
+ if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:')) {
+ return;
+ }
+
+ const absoluteUrl = new URL(href, baseUrl);
+
+ // Only include links from the same domain if configured
+ if (absoluteUrl.hostname === baseUrlObj.hostname) {
+ const urlString = absoluteUrl.href;
+ if (!this.visitedUrls.has(urlString)) {
+ links.push(urlString);
+ }
+ }
+ } catch (e) {
+ // Invalid URL, skip
+ }
+ });
+
+ return links;
+ }
+
+ getNextUrl(currentUrl, html, config) {
+ // Extract links from current page
+ const links = this.extractLinks(html, currentUrl);
+
+ // Add new links to queue (limit per page)
+ const linksToAdd = links.slice(0, config.linksPerPage || 10);
+ linksToAdd.forEach(link => {
+ if (!this.visitedUrls.has(link) && this.urlQueue.length < 100) {
+ this.urlQueue.push(link);
+ }
+ });
+
+ // Mark current URL as visited
+ this.visitedUrls.add(currentUrl);
+
+ // Get next URL from queue
+ if (this.urlQueue.length > 0) {
+ const randomIndex = Math.floor(Math.random() * this.urlQueue.length);
+ return this.urlQueue.splice(randomIndex, 1)[0];
+ }
+
+ return null;
+ }
+
+ reset() {
+ this.visitedUrls.clear();
+ this.urlQueue = [];
+ }
+}
+
+// ===================================
+// UTILITY FUNCTIONS
+// ===================================
+function calculatePercentile(arr, percentile) {
+ if (arr.length === 0) return 0;
+ const sorted = [...arr].sort((a, b) => a - b);
+ const index = Math.ceil((percentile / 100) * sorted.length) - 1;
+ return sorted[Math.max(0, index)];
+}
+
+function categorizeError(statusCode, errorMessage) {
+ if (statusCode >= 400 && statusCode < 500) return '4xx';
+ if (statusCode >= 500) return '5xx';
+ if (errorMessage && errorMessage.includes('timeout')) return 'timeout';
+ return 'network';
+}
+
+function formatBytes(bytes) {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+}
+
+// ===================================
+// MAIN STRESS TESTING TOOL CLASS
+// ===================================
+class StressTestingTool {
+ constructor() {
+ this.config = {
+ targetUrl: '',
+ userCount: 100,
+ duration: 60,
+ trafficPattern: 'steady',
+ httpMethod: 'GET',
+ customHeaders: {},
+ requestBody: null,
+ thinkTime: 1000,
+ proxyUrl: 'http://localhost:3000',
+
+ // Crawler settings
+ crawlerEnabled: false,
+ crawlDepth: 2,
+ linksPerPage: 10,
+ stayOnDomain: true
+ };
+
+ this.state = {
+ status: 'idle',
+ startTime: null,
+ pauseTime: null,
+ elapsedTime: 0,
+ activeUsers: 0,
+ totalRequests: 0,
+ successfulRequests: 0,
+ failedRequests: 0,
+ responseTimes: [],
+ requestsPerSecond: [],
+ workers: [],
+ updateInterval: null,
+ chartUpdateInterval: null,
+ userErrorData: [],
+ errorThreshold: null,
+
+ // Enhanced metrics
+ errorsByCategory: {
+ '4xx': 0,
+ '5xx': 0,
+ 'timeout': 0,
+ 'network': 0
+ },
+ totalBytesSent: 0,
+ totalBytesReceived: 0,
+ requestHistory: [],
+
+ // Percentile tracking
+ percentiles: {
+ p50: 0,
+ p95: 0,
+ p99: 0
+ }
+ };
+
+ this.crawler = new WebsiteCrawler();
+ this.charts = {
+ rps: null,
+ responseTime: null,
+ userError: null
+ };
+
+ // Test presets
+ this.presets = {
+ 'light': { userCount: 10, duration: 30, trafficPattern: 'steady' },
+ 'medium': { userCount: 100, duration: 60, trafficPattern: 'random' },
+ 'heavy': { userCount: 500, duration: 120, trafficPattern: 'rampup' },
+ 'spike': { userCount: 200, duration: 60, trafficPattern: 'burst' }
+ };
+
+ this.init();
+ }
+
+ init() {
+ this.bindElements();
+ this.attachEventListeners();
+ this.initializeCharts();
+ this.loadTheme();
+ this.loadSavedConfigs();
+ this.setupKeyboardShortcuts();
+ }
+
+ bindElements() {
+ // Form inputs
+ this.elements = {
+ targetUrl: document.getElementById('targetUrl'),
+ userCount: document.getElementById('userCount'),
+ userCountValue: document.getElementById('userCountValue'),
+ duration: document.getElementById('duration'),
+ durationValue: document.getElementById('durationValue'),
+ trafficPattern: document.getElementById('trafficPattern'),
+ httpMethod: document.getElementById('httpMethod'),
+ customHeaders: document.getElementById('customHeaders'),
+ requestBody: document.getElementById('requestBody'),
+ thinkTime: document.getElementById('thinkTime'),
+ thinkTimeValue: document.getElementById('thinkTimeValue'),
+
+ // Crawler controls
+ crawlerEnabled: document.getElementById('crawlerEnabled'),
+ crawlDepth: document.getElementById('crawlDepth'),
+ crawlDepthValue: document.getElementById('crawlDepthValue'),
+ linksPerPage: document.getElementById('linksPerPage'),
+ linksPerPageValue: document.getElementById('linksPerPageValue'),
+
+ // Controls
+ startBtn: document.getElementById('startBtn'),
+ pauseBtn: document.getElementById('pauseBtn'),
+ stopBtn: document.getElementById('stopBtn'),
+ statusBadge: document.getElementById('statusBadge'),
+ progressBar: document.getElementById('progressBar'),
+
+ // Statistics
+ elapsedTime: document.getElementById('elapsedTime'),
+ remainingTime: document.getElementById('remainingTime'),
+ activeUsers: document.getElementById('activeUsers'),
+ totalRequests: document.getElementById('totalRequests'),
+ requestsPerSec: document.getElementById('requestsPerSec'),
+ successRate: document.getElementById('successRate'),
+ failedRequests: document.getElementById('failedRequests'),
+ avgResponseTime: document.getElementById('avgResponseTime'),
+
+ // Enhanced metrics
+ p50ResponseTime: document.getElementById('p50ResponseTime'),
+ p95ResponseTime: document.getElementById('p95ResponseTime'),
+ p99ResponseTime: document.getElementById('p99ResponseTime'),
+ errors4xx: document.getElementById('errors4xx'),
+ errors5xx: document.getElementById('errors5xx'),
+ errorsTimeout: document.getElementById('errorsTimeout'),
+ errorsNetwork: document.getElementById('errorsNetwork'),
+ totalBandwidth: document.getElementById('totalBandwidth'),
+
+ // Request history
+ requestHistoryBody: document.getElementById('requestHistoryBody'),
+
+ // Results
+ resultsPanel: document.getElementById('resultsPanel'),
+ resultsTableBody: document.getElementById('resultsTableBody'),
+ exportJsonBtn: document.getElementById('exportJsonBtn'),
+ exportCsvBtn: document.getElementById('exportCsvBtn'),
+
+ // Advanced options
+ advancedToggle: document.getElementById('advancedToggle'),
+ advancedContent: document.getElementById('advancedContent'),
+
+ // Theme & presets
+ themeToggle: document.getElementById('themeToggle'),
+ presetSelect: document.getElementById('presetSelect'),
+ saveConfigBtn: document.getElementById('saveConfigBtn')
+ };
+ }
+
+ attachEventListeners() {
+ // Range inputs
+ this.elements.userCount.addEventListener('input', (e) => {
+ this.elements.userCountValue.textContent = e.target.value;
+ });
+
+ this.elements.duration.addEventListener('input', (e) => {
+ this.elements.durationValue.textContent = e.target.value;
+ });
+
+ this.elements.thinkTime.addEventListener('input', (e) => {
+ this.elements.thinkTimeValue.textContent = e.target.value;
+ });
+
+ if (this.elements.crawlDepth) {
+ this.elements.crawlDepth.addEventListener('input', (e) => {
+ this.elements.crawlDepthValue.textContent = e.target.value;
+ });
+ }
+
+ if (this.elements.linksPerPage) {
+ this.elements.linksPerPage.addEventListener('input', (e) => {
+ this.elements.linksPerPageValue.textContent = e.target.value;
+ });
+ }
+
+ // Control buttons
+ this.elements.startBtn.addEventListener('click', () => this.startTest());
+ this.elements.pauseBtn.addEventListener('click', () => this.pauseTest());
+ this.elements.stopBtn.addEventListener('click', () => this.stopTest());
+
+ // Export buttons
+ this.elements.exportJsonBtn.addEventListener('click', () => this.exportResults('json'));
+ this.elements.exportCsvBtn.addEventListener('click', () => this.exportResults('csv'));
+
+ // Advanced options accordion
+ this.elements.advancedToggle.addEventListener('click', () => {
+ this.elements.advancedToggle.classList.toggle('active');
+ this.elements.advancedContent.classList.toggle('active');
+ });
+
+ // Theme toggle
+ if (this.elements.themeToggle) {
+ this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
+ }
+
+ // Preset selector
+ if (this.elements.presetSelect) {
+ this.elements.presetSelect.addEventListener('change', (e) => this.loadPreset(e.target.value));
+ }
+
+ // Save config
+ if (this.elements.saveConfigBtn) {
+ this.elements.saveConfigBtn.addEventListener('click', () => this.saveConfig());
+ }
+ }
+
+ setupKeyboardShortcuts() {
+ document.addEventListener('keydown', (e) => {
+ // Don't trigger if user is typing in an input
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+
+ switch (e.key.toLowerCase()) {
+ case 's':
+ if (this.state.status === 'idle') this.startTest();
+ break;
+ case 'p':
+ if (this.state.status === 'running' || this.state.status === 'paused') this.pauseTest();
+ break;
+ case 'x':
+ if (this.state.status === 'running' || this.state.status === 'paused') this.stopTest();
+ break;
+ }
+ });
+ }
+
+ loadTheme() {
+ const savedTheme = localStorage.getItem('stressTestTheme') || 'dark';
+ document.documentElement.setAttribute('data-theme', savedTheme);
+ }
+
+ toggleTheme() {
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+ document.documentElement.setAttribute('data-theme', newTheme);
+ localStorage.setItem('stressTestTheme', newTheme);
+
+ // Update chart colors
+ this.updateChartTheme();
+ }
+
+ updateChartTheme() {
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ const textColor = isDark ? '#94a3b8' : '#475569';
+ const gridColor = isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(148, 163, 184, 0.2)';
+
+ Object.values(this.charts).forEach(chart => {
+ if (chart) {
+ chart.options.scales.x.ticks.color = textColor;
+ chart.options.scales.x.grid.color = gridColor;
+ chart.options.scales.y.ticks.color = textColor;
+ chart.options.scales.y.grid.color = gridColor;
+ if (chart.options.scales.y1) {
+ chart.options.scales.y1.ticks.color = textColor;
+ }
+ chart.update('none');
+ }
+ });
+ }
+
+ loadSavedConfigs() {
+ const saved = localStorage.getItem('stressTestConfigs');
+ if (saved) {
+ try {
+ const configs = JSON.parse(saved);
+ // Add to preset select if exists
+ if (this.elements.presetSelect) {
+ Object.keys(configs).forEach(name => {
+ const option = document.createElement('option');
+ option.value = `saved_${name}`;
+ option.textContent = `š¾ ${name}`;
+ this.elements.presetSelect.appendChild(option);
+ });
+ }
+ } catch (e) {
+ console.error('Failed to load saved configs:', e);
+ }
+ }
+ }
+
+ loadPreset(presetName) {
+ if (!presetName) return;
+
+ let config;
+ if (presetName.startsWith('saved_')) {
+ const saved = JSON.parse(localStorage.getItem('stressTestConfigs') || '{}');
+ config = saved[presetName.replace('saved_', '')];
+ } else {
+ config = this.presets[presetName];
+ }
+
+ if (config) {
+ this.elements.userCount.value = config.userCount;
+ this.elements.userCountValue.textContent = config.userCount;
+ this.elements.duration.value = config.duration;
+ this.elements.durationValue.textContent = config.duration;
+ this.elements.trafficPattern.value = config.trafficPattern;
+ }
+ }
+
+ saveConfig() {
+ const name = prompt('Enter a name for this configuration:');
+ if (!name) return;
+
+ const config = {
+ userCount: parseInt(this.elements.userCount.value),
+ duration: parseInt(this.elements.duration.value),
+ trafficPattern: this.elements.trafficPattern.value,
+ targetUrl: this.elements.targetUrl.value
+ };
+
+ const saved = JSON.parse(localStorage.getItem('stressTestConfigs') || '{}');
+ saved[name] = config;
+ localStorage.setItem('stressTestConfigs', JSON.stringify(saved));
+
+ alert(`Configuration "${name}" saved!`);
+ location.reload(); // Reload to update preset list
+ }
+
+ initializeCharts() {
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ const textColor = isDark ? '#94a3b8' : '#475569';
+ const gridColor = isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(148, 163, 184, 0.2)';
+
+ const chartOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ }
+ },
+ scales: {
+ x: {
+ grid: {
+ color: gridColor
+ },
+ ticks: {
+ color: textColor
+ }
+ },
+ y: {
+ grid: {
+ color: gridColor
+ },
+ ticks: {
+ color: textColor
+ },
+ beginAtZero: true
+ }
+ }
+ };
+
+ // RPS Chart
+ const rpsCtx = document.getElementById('rpsChart').getContext('2d');
+ this.charts.rps = new Chart(rpsCtx, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Requests per Second',
+ data: [],
+ borderColor: '#6366f1',
+ backgroundColor: 'rgba(99, 102, 241, 0.1)',
+ borderWidth: 2,
+ fill: true,
+ tension: 0.4
+ }]
+ },
+ options: {
+ ...chartOptions,
+ plugins: {
+ ...chartOptions.plugins,
+ title: {
+ display: true,
+ text: 'Requests per Second',
+ color: textColor,
+ font: {
+ size: 14,
+ weight: 600
+ }
+ }
+ }
+ }
+ });
+
+ // Response Time Chart
+ const responseTimeCtx = document.getElementById('responseTimeChart').getContext('2d');
+ this.charts.responseTime = new Chart(responseTimeCtx, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Average Response Time (ms)',
+ data: [],
+ borderColor: '#f59e0b',
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
+ borderWidth: 2,
+ fill: true,
+ tension: 0.4
+ }]
+ },
+ options: {
+ ...chartOptions,
+ plugins: {
+ ...chartOptions.plugins,
+ title: {
+ display: true,
+ text: 'Average Response Time',
+ color: textColor,
+ font: {
+ size: 14,
+ weight: 600
+ }
+ }
+ }
+ }
+ });
+
+ // User/Error Correlation Chart
+ const userErrorCtx = document.getElementById('userErrorChart').getContext('2d');
+ this.charts.userError = new Chart(userErrorCtx, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: 'Active Users',
+ data: [],
+ borderColor: '#3b82f6',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ borderWidth: 2,
+ fill: true,
+ tension: 0.4,
+ yAxisID: 'y'
+ },
+ {
+ label: 'Error Rate (%)',
+ data: [],
+ borderColor: '#ef4444',
+ backgroundColor: 'rgba(239, 68, 68, 0.2)',
+ borderWidth: 2,
+ fill: true,
+ tension: 0.4,
+ yAxisID: 'y1'
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: {
+ mode: 'index',
+ intersect: false
+ },
+ plugins: {
+ legend: {
+ display: true,
+ labels: {
+ color: textColor,
+ font: {
+ size: 12,
+ weight: 600
+ }
+ }
+ },
+ title: {
+ display: true,
+ text: 'User Load vs Error Rate',
+ color: textColor,
+ font: {
+ size: 14,
+ weight: 600
+ }
+ }
+ },
+ scales: {
+ x: {
+ grid: {
+ color: gridColor
+ },
+ ticks: {
+ color: textColor
+ }
+ },
+ y: {
+ type: 'linear',
+ display: true,
+ position: 'left',
+ title: {
+ display: true,
+ text: 'Active Users',
+ color: '#3b82f6',
+ font: {
+ size: 12,
+ weight: 600
+ }
+ },
+ grid: {
+ color: gridColor
+ },
+ ticks: {
+ color: textColor
+ },
+ beginAtZero: true
+ },
+ y1: {
+ type: 'linear',
+ display: true,
+ position: 'right',
+ title: {
+ display: true,
+ text: 'Error Rate (%)',
+ color: '#ef4444',
+ font: {
+ size: 12,
+ weight: 600
+ }
+ },
+ grid: {
+ drawOnChartArea: false
+ },
+ ticks: {
+ color: textColor
+ },
+ beginAtZero: true,
+ max: 100
+ }
+ }
+ }
+ });
+ }
+
+ async startTest() {
+ if (!this.validateConfig()) {
+ return;
+ }
+
+ this.gatherConfig();
+ this.resetState();
+ this.updateStatus('running');
+ this.state.startTime = Date.now();
+
+ // Update UI
+ this.elements.startBtn.disabled = true;
+ this.elements.pauseBtn.disabled = false;
+ this.elements.stopBtn.disabled = false;
+
+ // Start workers
+ this.startWorkers();
+
+ // Start update intervals
+ this.state.updateInterval = setInterval(() => this.updateStatistics(), 100);
+ this.state.chartUpdateInterval = setInterval(() => this.updateCharts(), 1000);
+ }
+
+ pauseTest() {
+ if (this.state.status === 'running') {
+ this.updateStatus('paused');
+ this.state.pauseTime = Date.now();
+ this.stopWorkers();
+ this.elements.pauseBtn.textContent = 'ā¶ļø Resume';
+ } else if (this.state.status === 'paused') {
+ this.updateStatus('running');
+ const pauseDuration = Date.now() - this.state.pauseTime;
+ this.state.startTime += pauseDuration;
+ this.startWorkers();
+ this.elements.pauseBtn.textContent = 'āøļø Pause';
+ }
+ }
+
+ stopTest() {
+ this.updateStatus('stopped');
+ this.stopWorkers();
+ clearInterval(this.state.updateInterval);
+ clearInterval(this.state.chartUpdateInterval);
+
+ // Update UI
+ this.elements.startBtn.disabled = false;
+ this.elements.pauseBtn.disabled = true;
+ this.elements.stopBtn.disabled = true;
+ this.elements.pauseBtn.textContent = 'āøļø Pause';
+
+ // Calculate final percentiles
+ this.calculatePercentiles();
+
+ // Show results
+ this.displayResults();
+ }
+
+ validateConfig() {
+ const url = this.elements.targetUrl.value.trim();
+ if (!url) {
+ alert('Please enter a target URL');
+ return false;
+ }
+
+ try {
+ new URL(url);
+ } catch (e) {
+ alert('Please enter a valid URL');
+ return false;
+ }
+
+ const headersText = this.elements.customHeaders.value.trim();
+ if (headersText) {
+ try {
+ JSON.parse(headersText);
+ } catch (e) {
+ alert('Custom headers must be valid JSON');
+ return false;
+ }
+ }
+
+ const bodyText = this.elements.requestBody.value.trim();
+ if (bodyText) {
+ try {
+ JSON.parse(bodyText);
+ } catch (e) {
+ alert('Request body must be valid JSON');
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ gatherConfig() {
+ this.config.targetUrl = this.elements.targetUrl.value.trim();
+ this.config.userCount = parseInt(this.elements.userCount.value);
+ this.config.duration = parseInt(this.elements.duration.value);
+ this.config.trafficPattern = this.elements.trafficPattern.value;
+ this.config.httpMethod = this.elements.httpMethod.value;
+ this.config.thinkTime = parseInt(this.elements.thinkTime.value);
+
+ const headersText = this.elements.customHeaders.value.trim();
+ this.config.customHeaders = headersText ? JSON.parse(headersText) : {};
+
+ const bodyText = this.elements.requestBody.value.trim();
+ this.config.requestBody = bodyText ? JSON.parse(bodyText) : null;
+
+ // Crawler config
+ if (this.elements.crawlerEnabled) {
+ this.config.crawlerEnabled = this.elements.crawlerEnabled.checked;
+ this.config.crawlDepth = parseInt(this.elements.crawlDepth?.value || 2);
+ this.config.linksPerPage = parseInt(this.elements.linksPerPage?.value || 10);
+ }
+ }
+
+ resetState() {
+ this.state.elapsedTime = 0;
+ this.state.activeUsers = 0;
+ this.state.totalRequests = 0;
+ this.state.successfulRequests = 0;
+ this.state.failedRequests = 0;
+ this.state.responseTimes = [];
+ this.state.requestsPerSecond = [];
+ this.state.workers = [];
+ this.state.userErrorData = [];
+ this.state.errorThreshold = null;
+ this.state.errorsByCategory = { '4xx': 0, '5xx': 0, 'timeout': 0, 'network': 0 };
+ this.state.totalBytesSent = 0;
+ this.state.totalBytesReceived = 0;
+ this.state.requestHistory = [];
+ this.state.percentiles = { p50: 0, p95: 0, p99: 0 };
+
+ // Reset crawler
+ this.crawler.reset();
+ if (this.config.crawlerEnabled) {
+ this.crawler.urlQueue.push(this.config.targetUrl);
+ }
+
+ // Reset charts
+ this.charts.rps.data.labels = [];
+ this.charts.rps.data.datasets[0].data = [];
+ this.charts.responseTime.data.labels = [];
+ this.charts.responseTime.data.datasets[0].data = [];
+ this.charts.userError.data.labels = [];
+ this.charts.userError.data.datasets[0].data = [];
+ this.charts.userError.data.datasets[1].data = [];
+ this.charts.rps.update('none');
+ this.charts.responseTime.update('none');
+ this.charts.userError.update('none');
+
+ // Clear request history table
+ if (this.elements.requestHistoryBody) {
+ this.elements.requestHistoryBody.innerHTML = '';
+ }
+
+ // Hide results panel
+ this.elements.resultsPanel.style.display = 'none';
+ }
+
+ startWorkers() {
+ const pattern = this.config.trafficPattern;
+
+ switch (pattern) {
+ case 'steady':
+ this.startSteadyLoad();
+ break;
+ case 'burst':
+ this.startBurstLoad();
+ break;
+ case 'rampup':
+ this.startRampUpLoad();
+ break;
+ case 'random':
+ this.startRandomLoad();
+ break;
+ }
+ }
+
+ startSteadyLoad() {
+ const delayBetweenUsers = 100;
+
+ for (let i = 0; i < this.config.userCount; i++) {
+ setTimeout(() => {
+ if (this.state.status === 'running') {
+ this.createWorker(i);
+ }
+ }, i * delayBetweenUsers);
+ }
+ }
+
+ startBurstLoad() {
+ const burstSize = Math.ceil(this.config.userCount / 5);
+ const burstInterval = (this.config.duration * 1000) / 5;
+
+ for (let burst = 0; burst < 5; burst++) {
+ setTimeout(() => {
+ if (this.state.status === 'running') {
+ for (let i = 0; i < burstSize; i++) {
+ this.createWorker(burst * burstSize + i);
+ }
+ }
+ }, burst * burstInterval);
+ }
+ }
+
+ startRampUpLoad() {
+ const totalTime = this.config.duration * 1000;
+ const timePerUser = totalTime / this.config.userCount;
+
+ for (let i = 0; i < this.config.userCount; i++) {
+ setTimeout(() => {
+ if (this.state.status === 'running') {
+ this.createWorker(i);
+ }
+ }, i * timePerUser);
+ }
+ }
+
+ startRandomLoad() {
+ const maxDelay = (this.config.duration * 1000) / 2;
+
+ for (let i = 0; i < this.config.userCount; i++) {
+ const randomDelay = Math.random() * maxDelay;
+ setTimeout(() => {
+ if (this.state.status === 'running') {
+ this.createWorker(i);
+ }
+ }, randomDelay);
+ }
+ }
+
+ createWorker(id) {
+ const worker = {
+ id: id,
+ active: true,
+ requestCount: 0,
+ currentUrl: this.config.targetUrl,
+ crawlDepth: 0
+ };
+
+ this.state.workers.push(worker);
+ this.state.activeUsers++;
+ this.runWorker(worker);
+ }
+
+ async runWorker(worker) {
+ const endTime = this.state.startTime + (this.config.duration * 1000);
+
+ while (worker.active && this.state.status === 'running' && Date.now() < endTime) {
+ await this.makeRequest(worker);
+
+ // Think time
+ if (this.config.thinkTime > 0) {
+ await this.sleep(this.config.thinkTime);
+ }
+ }
+
+ // Worker finished
+ worker.active = false;
+ this.state.activeUsers--;
+
+ // Check if all workers are done
+ if (this.state.activeUsers === 0 && this.state.status === 'running') {
+ this.stopTest();
+ }
+ }
+
+ async makeRequest(worker) {
+ const startTime = performance.now();
+ const requestUrl = worker.currentUrl;
+
+ try {
+ const proxyPayload = {
+ targetUrl: requestUrl,
+ method: this.config.httpMethod,
+ headers: this.config.customHeaders,
+ body: this.config.requestBody
+ };
+
+ // Estimate request size
+ const requestSize = JSON.stringify(proxyPayload).length;
+ this.state.totalBytesSent += requestSize;
+
+ const response = await fetch(this.config.proxyUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(proxyPayload)
+ });
+
+ const endTime = performance.now();
+ const responseTime = endTime - startTime;
+
+ this.state.totalRequests++;
+ worker.requestCount++;
+
+ const proxyResponse = await response.json();
+
+ // Track response size
+ if (proxyResponse.body) {
+ this.state.totalBytesReceived += proxyResponse.body.length;
+ }
+
+ const isSuccess = proxyResponse.success && proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 400;
+
+ if (isSuccess) {
+ this.state.successfulRequests++;
+ } else {
+ this.state.failedRequests++;
+ const category = categorizeError(proxyResponse.statusCode, proxyResponse.error);
+ this.state.errorsByCategory[category]++;
+ }
+
+ const actualResponseTime = proxyResponse.responseTime || responseTime;
+ this.state.responseTimes.push(actualResponseTime);
+
+ // Keep only last 1000 response times
+ if (this.state.responseTimes.length > 1000) {
+ this.state.responseTimes.shift();
+ }
+
+ // Add to request history
+ this.addToRequestHistory({
+ url: requestUrl,
+ status: proxyResponse.statusCode,
+ responseTime: Math.round(actualResponseTime),
+ success: isSuccess,
+ timestamp: new Date().toLocaleTimeString()
+ });
+
+ // Crawler: Get next URL if enabled
+ if (this.config.crawlerEnabled && isSuccess && proxyResponse.body && worker.crawlDepth < this.config.crawlDepth) {
+ const nextUrl = this.crawler.getNextUrl(requestUrl, proxyResponse.body, this.config);
+ if (nextUrl) {
+ worker.currentUrl = nextUrl;
+ worker.crawlDepth++;
+ }
+ }
+
+ } catch (error) {
+ const endTime = performance.now();
+ const responseTime = endTime - startTime;
+
+ this.state.totalRequests++;
+ this.state.failedRequests++;
+ this.state.responseTimes.push(responseTime);
+ this.state.errorsByCategory['network']++;
+ worker.requestCount++;
+
+ this.addToRequestHistory({
+ url: requestUrl,
+ status: 0,
+ responseTime: Math.round(responseTime),
+ success: false,
+ timestamp: new Date().toLocaleTimeString(),
+ error: error.message
+ });
+ }
+ }
+
+ addToRequestHistory(request) {
+ this.state.requestHistory.unshift(request);
+
+ // Keep only last 100
+ if (this.state.requestHistory.length > 100) {
+ this.state.requestHistory.pop();
+ }
+
+ // Update UI table
+ if (this.elements.requestHistoryBody) {
+ const row = document.createElement('tr');
+ row.className = request.success ? 'success-row' : 'error-row';
+ row.innerHTML = `
+ ${request.timestamp}
+ ${this.truncateUrl(request.url)}
+ ${request.status}
+ ${request.responseTime}ms
+ `;
+
+ this.elements.requestHistoryBody.insertBefore(row, this.elements.requestHistoryBody.firstChild);
+
+ // Keep only 100 rows in DOM
+ while (this.elements.requestHistoryBody.children.length > 100) {
+ this.elements.requestHistoryBody.removeChild(this.elements.requestHistoryBody.lastChild);
+ }
+ }
+ }
+
+ truncateUrl(url) {
+ if (url.length > 50) {
+ return url.substring(0, 47) + '...';
+ }
+ return url;
+ }
+
+ stopWorkers() {
+ this.state.workers.forEach(worker => {
+ worker.active = false;
+ });
+ }
+
+ calculatePercentiles() {
+ if (this.state.responseTimes.length > 0) {
+ this.state.percentiles.p50 = Math.round(calculatePercentile(this.state.responseTimes, 50));
+ this.state.percentiles.p95 = Math.round(calculatePercentile(this.state.responseTimes, 95));
+ this.state.percentiles.p99 = Math.round(calculatePercentile(this.state.responseTimes, 99));
+ }
+ }
+
+ updateStatistics() {
+ const now = Date.now();
+ const elapsed = Math.floor((now - this.state.startTime) / 1000);
+ const remaining = Math.max(0, this.config.duration - elapsed);
+ const progress = Math.min(100, (elapsed / this.config.duration) * 100);
+
+ // Update time displays
+ this.elements.elapsedTime.textContent = `${elapsed}s`;
+ this.elements.remainingTime.textContent = `${remaining}s`;
+ this.elements.progressBar.style.width = `${progress}%`;
+
+ // Update statistics
+ this.elements.activeUsers.textContent = this.state.activeUsers;
+ this.elements.totalRequests.textContent = this.state.totalRequests.toLocaleString();
+ this.elements.failedRequests.textContent = this.state.failedRequests.toLocaleString();
+
+ // Calculate RPS
+ const rps = elapsed > 0 ? Math.round(this.state.totalRequests / elapsed) : 0;
+ this.elements.requestsPerSec.textContent = rps;
+
+ // Calculate success rate
+ const successRate = this.state.totalRequests > 0
+ ? ((this.state.successfulRequests / this.state.totalRequests) * 100).toFixed(1)
+ : 0;
+ this.elements.successRate.textContent = `${successRate}%`;
+
+ // Calculate average response time
+ const avgResponseTime = this.state.responseTimes.length > 0
+ ? Math.round(this.state.responseTimes.reduce((a, b) => a + b, 0) / this.state.responseTimes.length)
+ : 0;
+ this.elements.avgResponseTime.textContent = `${avgResponseTime}ms`;
+
+ // Update enhanced metrics
+ if (this.elements.p50ResponseTime) {
+ const p50 = Math.round(calculatePercentile(this.state.responseTimes, 50));
+ const p95 = Math.round(calculatePercentile(this.state.responseTimes, 95));
+ const p99 = Math.round(calculatePercentile(this.state.responseTimes, 99));
+
+ this.elements.p50ResponseTime.textContent = `${p50}ms`;
+ this.elements.p95ResponseTime.textContent = `${p95}ms`;
+ this.elements.p99ResponseTime.textContent = `${p99}ms`;
+ }
+
+ if (this.elements.errors4xx) {
+ this.elements.errors4xx.textContent = this.state.errorsByCategory['4xx'];
+ this.elements.errors5xx.textContent = this.state.errorsByCategory['5xx'];
+ this.elements.errorsTimeout.textContent = this.state.errorsByCategory['timeout'];
+ this.elements.errorsNetwork.textContent = this.state.errorsByCategory['network'];
+ }
+
+ if (this.elements.totalBandwidth) {
+ const totalBytes = this.state.totalBytesSent + this.state.totalBytesReceived;
+ this.elements.totalBandwidth.textContent = formatBytes(totalBytes);
+ }
+ }
+
+ updateCharts() {
+ const now = Date.now();
+ const elapsed = Math.floor((now - this.state.startTime) / 1000);
+
+ // Calculate current RPS
+ const currentRps = this.state.totalRequests > 0 && elapsed > 0
+ ? Math.round(this.state.totalRequests / elapsed)
+ : 0;
+
+ // Calculate current average response time
+ const recentResponseTimes = this.state.responseTimes.slice(-100);
+ const currentAvgResponseTime = recentResponseTimes.length > 0
+ ? Math.round(recentResponseTimes.reduce((a, b) => a + b, 0) / recentResponseTimes.length)
+ : 0;
+
+ // Update RPS chart
+ this.charts.rps.data.labels.push(`${elapsed}s`);
+ this.charts.rps.data.datasets[0].data.push(currentRps);
+
+ if (this.charts.rps.data.labels.length > 60) {
+ this.charts.rps.data.labels.shift();
+ this.charts.rps.data.datasets[0].data.shift();
+ }
+
+ this.charts.rps.update('none');
+
+ // Update Response Time chart
+ this.charts.responseTime.data.labels.push(`${elapsed}s`);
+ this.charts.responseTime.data.datasets[0].data.push(currentAvgResponseTime);
+
+ if (this.charts.responseTime.data.labels.length > 60) {
+ this.charts.responseTime.data.labels.shift();
+ this.charts.responseTime.data.datasets[0].data.shift();
+ }
+
+ this.charts.responseTime.update('none');
+
+ // Calculate current error rate
+ const currentErrorRate = this.state.totalRequests > 0
+ ? ((this.state.failedRequests / this.state.totalRequests) * 100).toFixed(1)
+ : 0;
+
+ // Update User/Error chart
+ this.charts.userError.data.labels.push(`${elapsed}s`);
+ this.charts.userError.data.datasets[0].data.push(this.state.activeUsers);
+ this.charts.userError.data.datasets[1].data.push(parseFloat(currentErrorRate));
+
+ // Track user/error data
+ this.state.userErrorData.push({
+ time: elapsed,
+ users: this.state.activeUsers,
+ errorRate: parseFloat(currentErrorRate),
+ failedRequests: this.state.failedRequests
+ });
+
+ // Detect error threshold
+ if (this.state.errorThreshold === null && this.state.failedRequests > 0 && this.state.activeUsers > 0) {
+ this.state.errorThreshold = {
+ users: this.state.activeUsers,
+ time: elapsed,
+ errorRate: parseFloat(currentErrorRate)
+ };
+ }
+
+ if (this.charts.userError.data.labels.length > 60) {
+ this.charts.userError.data.labels.shift();
+ this.charts.userError.data.datasets[0].data.shift();
+ this.charts.userError.data.datasets[1].data.shift();
+ }
+
+ this.charts.userError.update('none');
+ }
+
+ updateStatus(status) {
+ this.state.status = status;
+ const badge = this.elements.statusBadge;
+
+ badge.className = 'status-badge';
+
+ switch (status) {
+ case 'idle':
+ badge.classList.add('status-idle');
+ badge.textContent = 'Idle';
+ break;
+ case 'running':
+ badge.classList.add('status-running');
+ badge.textContent = 'Running';
+ break;
+ case 'paused':
+ badge.classList.add('status-paused');
+ badge.textContent = 'Paused';
+ break;
+ case 'stopped':
+ badge.classList.add('status-idle');
+ badge.textContent = 'Completed';
+ break;
+ }
+ }
+
+ displayResults() {
+ this.elements.resultsPanel.style.display = 'block';
+
+ const results = this.calculateResults();
+ const tbody = this.elements.resultsTableBody;
+ tbody.innerHTML = '';
+
+ // Populate results table
+ Object.entries(results).forEach(([key, value]) => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${key}
+ ${value}
+ `;
+ tbody.appendChild(row);
+ });
+
+ // Scroll to results
+ this.elements.resultsPanel.scrollIntoView({ behavior: 'smooth' });
+ }
+
+ calculateResults() {
+ const totalTime = this.state.elapsedTime || Math.floor((Date.now() - this.state.startTime) / 1000);
+ const successRate = this.state.totalRequests > 0
+ ? ((this.state.successfulRequests / this.state.totalRequests) * 100).toFixed(2)
+ : 0;
+
+ const avgResponseTime = this.state.responseTimes.length > 0
+ ? Math.round(this.state.responseTimes.reduce((a, b) => a + b, 0) / this.state.responseTimes.length)
+ : 0;
+
+ const minResponseTime = this.state.responseTimes.length > 0
+ ? Math.round(Math.min(...this.state.responseTimes))
+ : 0;
+
+ const maxResponseTime = this.state.responseTimes.length > 0
+ ? Math.round(Math.max(...this.state.responseTimes))
+ : 0;
+
+ const rps = totalTime > 0 ? (this.state.totalRequests / totalTime).toFixed(2) : 0;
+
+ const results = {
+ 'Target URL': this.config.targetUrl,
+ 'Test Duration': `${totalTime} seconds`,
+ 'Concurrent Users': this.config.userCount,
+ 'Traffic Pattern': this.config.trafficPattern,
+ 'Crawler Mode': this.config.crawlerEnabled ? 'Enabled' : 'Disabled',
+ 'Total Requests': this.state.totalRequests.toLocaleString(),
+ 'Successful Requests': this.state.successfulRequests.toLocaleString(),
+ 'Failed Requests': this.state.failedRequests.toLocaleString(),
+ 'Success Rate': `${successRate}%`,
+ 'Requests per Second': rps,
+ 'Average Response Time': `${avgResponseTime}ms`,
+ 'Min Response Time': `${minResponseTime}ms`,
+ 'Max Response Time': `${maxResponseTime}ms`,
+ 'P50 Response Time': `${this.state.percentiles.p50}ms`,
+ 'P95 Response Time': `${this.state.percentiles.p95}ms`,
+ 'P99 Response Time': `${this.state.percentiles.p99}ms`,
+ '4xx Errors': this.state.errorsByCategory['4xx'],
+ '5xx Errors': this.state.errorsByCategory['5xx'],
+ 'Timeout Errors': this.state.errorsByCategory['timeout'],
+ 'Network Errors': this.state.errorsByCategory['network'],
+ 'Total Bandwidth': formatBytes(this.state.totalBytesSent + this.state.totalBytesReceived),
+ 'Data Sent': formatBytes(this.state.totalBytesSent),
+ 'Data Received': formatBytes(this.state.totalBytesReceived),
+ 'HTTP Method': this.config.httpMethod,
+ 'Think Time': `${this.config.thinkTime}ms`,
+ 'Error Threshold': this.state.errorThreshold
+ ? `${this.state.errorThreshold.users} users at ${this.state.errorThreshold.time}s (${this.state.errorThreshold.errorRate}% error rate)`
+ : 'No errors detected'
+ };
+
+ if (this.config.crawlerEnabled) {
+ results['Unique URLs Visited'] = this.crawler.visitedUrls.size;
+ }
+
+ return results;
+ }
+
+ exportResults(format) {
+ const results = this.calculateResults();
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+
+ if (format === 'json') {
+ const data = {
+ config: this.config,
+ results: results,
+ requestHistory: this.state.requestHistory.slice(0, 100),
+ timestamp: new Date().toISOString()
+ };
+
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
+ this.downloadFile(blob, `stress-test-results-${timestamp}.json`);
+ } else if (format === 'csv') {
+ let csv = 'Metric,Value\n';
+ Object.entries(results).forEach(([key, value]) => {
+ csv += `"${key}","${value}"\n`;
+ });
+
+ const blob = new Blob([csv], { type: 'text/csv' });
+ this.downloadFile(blob, `stress-test-results-${timestamp}.csv`);
+ }
+ }
+
+ downloadFile(blob, filename) {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+
+ sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+}
+
+// Initialize the application
+document.addEventListener('DOMContentLoaded', () => {
+ new StressTestingTool();
+});
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..3d6c2d6
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,1046 @@
+/* ===================================
+ STRESS TESTING TOOL - DESIGN SYSTEM
+ Enhanced with Light Theme Support
+ =================================== */
+
+/* Import Modern Typography */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
+
+/* ===================================
+ CSS VARIABLES - DESIGN TOKENS
+ =================================== */
+:root {
+ /* Dark Theme Colors */
+ --color-bg-primary: #0a0e1a;
+ --color-bg-secondary: #131829;
+ --color-bg-tertiary: #1a2035;
+ --color-bg-glass: rgba(26, 32, 53, 0.7);
+ --color-bg-glass-hover: rgba(26, 32, 53, 0.85);
+
+ /* Accent Colors */
+ --color-accent-primary: #6366f1;
+ --color-accent-secondary: #8b5cf6;
+ --color-accent-success: #10b981;
+ --color-accent-warning: #f59e0b;
+ --color-accent-danger: #ef4444;
+ --color-accent-info: #3b82f6;
+
+ /* Gradients */
+ --gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ --gradient-success: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ --gradient-danger: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ --gradient-warning: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
+
+ /* Text Colors */
+ --color-text-primary: #f8fafc;
+ --color-text-secondary: #cbd5e1;
+ --color-text-tertiary: #94a3b8;
+ --color-text-muted: #64748b;
+
+ /* Border & Shadow */
+ --color-border: rgba(148, 163, 184, 0.1);
+ --color-border-hover: rgba(148, 163, 184, 0.2);
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
+ --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.3);
+
+ /* Spacing */
+ --spacing-xs: 0.5rem;
+ --spacing-sm: 0.75rem;
+ --spacing-md: 1rem;
+ --spacing-lg: 1.5rem;
+ --spacing-xl: 2rem;
+ --spacing-2xl: 3rem;
+
+ /* Border Radius */
+ --radius-sm: 0.375rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 0.75rem;
+ --radius-xl: 1rem;
+
+ /* Typography */
+ --font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ --font-mono: 'JetBrains Mono', 'Courier New', monospace;
+
+ /* Transitions */
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Light Theme */
+[data-theme="light"] {
+ --color-bg-primary: #f8fafc;
+ --color-bg-secondary: #f1f5f9;
+ --color-bg-tertiary: #e2e8f0;
+ --color-bg-glass: rgba(255, 255, 255, 0.8);
+ --color-bg-glass-hover: rgba(255, 255, 255, 0.95);
+
+ --color-text-primary: #0f172a;
+ --color-text-secondary: #334155;
+ --color-text-tertiary: #475569;
+ --color-text-muted: #64748b;
+
+ --color-border: rgba(148, 163, 184, 0.2);
+ --color-border-hover: rgba(148, 163, 184, 0.3);
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.15);
+}
+
+/* ===================================
+ GLOBAL RESET & BASE STYLES
+ =================================== */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 16px;
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: var(--font-primary);
+ background: var(--color-bg-primary);
+ color: var(--color-text-primary);
+ line-height: 1.6;
+ min-height: 100vh;
+ overflow-x: hidden;
+ position: relative;
+ transition: background var(--transition-base), color var(--transition-base);
+}
+
+/* Animated Background */
+body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background:
+ radial-gradient(circle at 20% 30%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 80% 70%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
+ pointer-events: none;
+ z-index: 0;
+}
+
+/* ===================================
+ LAYOUT STRUCTURE
+ =================================== */
+.container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: var(--spacing-xl);
+ position: relative;
+ z-index: 1;
+}
+
+/* Header */
+.header {
+ text-align: center;
+ margin-bottom: var(--spacing-lg);
+ animation: fadeInDown 0.6s ease-out;
+ position: relative;
+ display: block;
+}
+
+.header-content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.title {
+ font-size: 3rem;
+ font-weight: 700;
+ background: var(--gradient-primary);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: var(--spacing-xs);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 0;
+ line-height: 1.2;
+}
+
+.title-icon {
+ width: 2.5rem;
+ height: 2.5rem;
+ stroke: url(#gradient);
+ /* Note: SVG needs defs for this, or we use currentColor */
+ color: var(--color-accent-primary);
+ filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.5));
+ margin-bottom: var(--spacing-xs);
+}
+
+.subtitle {
+ font-size: 1.125rem;
+ color: var(--color-text-secondary);
+ font-weight: 300;
+ margin-bottom: var(--spacing-sm);
+}
+
+.header-controls {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+/* YouTube Link */
+.youtube-link {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-sm) var(--spacing-md);
+ background: linear-gradient(135deg, #FF0000 0%, #CC0000 100%);
+ color: white;
+ text-decoration: none;
+ border-radius: var(--radius-md);
+ font-weight: 600;
+ font-size: 0.875rem;
+ box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3);
+ transition: all var(--transition-base);
+ animation: pulse-glow 2s ease-in-out infinite;
+ margin-top: var(--spacing-md);
+}
+
+.youtube-link:hover {
+ transform: translateY(-2px) scale(1.03);
+ box-shadow: 0 4px 15px rgba(255, 0, 0, 0.5);
+ animation: none;
+ color: white;
+}
+
+.youtube-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ flex-shrink: 0;
+}
+
+@keyframes pulse-glow {
+
+ 0%,
+ 100% {
+ box-shadow: 0 2px 8px rgba(255, 0, 0, 0.3);
+ }
+
+ 50% {
+ box-shadow: 0 2px 15px rgba(255, 0, 0, 0.5);
+ }
+}
+
+/* Footer */
+.footer {
+ text-align: center;
+ margin-top: var(--spacing-2xl);
+ padding: var(--spacing-xl) 0;
+ border-top: 1px solid var(--color-border);
+ color: var(--color-text-muted);
+ font-size: 0.875rem;
+}
+
+/* Main Grid Layout */
+.main-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--spacing-xl);
+ margin-bottom: var(--spacing-xl);
+}
+
+/* ===================================
+ GLASS PANEL COMPONENT
+ =================================== */
+.panel {
+ background: var(--color-bg-glass);
+ backdrop-filter: blur(20px);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-xl);
+ padding: var(--spacing-xl);
+ box-shadow: var(--shadow-md);
+ transition: all var(--transition-base);
+}
+
+.panel:hover {
+ background: var(--color-bg-glass-hover);
+ border-color: var(--color-border-hover);
+ box-shadow: var(--shadow-lg);
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-lg);
+ padding-bottom: var(--spacing-md);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.panel-icon {
+ width: 2.5rem;
+ height: 2.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--gradient-primary);
+ border-radius: var(--radius-md);
+ font-size: 1.25rem;
+}
+
+.panel-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--color-text-primary);
+}
+
+.panel-section {
+ margin-top: var(--spacing-lg);
+}
+
+.section-title {
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ margin-bottom: var(--spacing-md);
+}
+
+/* ===================================
+ PRESET CONTROLS
+ =================================== */
+.preset-controls {
+ display: flex;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-md);
+}
+
+.preset-controls select {
+ flex: 1;
+}
+
+.keyboard-shortcuts {
+ padding: var(--spacing-sm);
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-md);
+ text-align: center;
+}
+
+.keyboard-shortcuts small {
+ color: var(--color-text-tertiary);
+}
+
+/* ===================================
+ FORM CONTROLS
+ =================================== */
+.form-group {
+ margin-bottom: var(--spacing-lg);
+}
+
+.form-label {
+ display: block;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ margin-bottom: var(--spacing-xs);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.form-input,
+.form-select,
+.form-textarea {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ color: var(--color-text-primary);
+ font-family: var(--font-primary);
+ font-size: 0.9375rem;
+ transition: all var(--transition-base);
+}
+
+.form-input:focus,
+.form-select:focus,
+.form-textarea:focus {
+ outline: none;
+ border-color: var(--color-accent-primary);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
+}
+
+.form-input::placeholder {
+ color: var(--color-text-muted);
+}
+
+.form-textarea {
+ resize: vertical;
+ min-height: 100px;
+ font-family: var(--font-mono);
+ font-size: 0.875rem;
+}
+
+.help-text {
+ display: block;
+ margin-top: var(--spacing-xs);
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+}
+
+/* Checkbox */
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.form-checkbox {
+ width: 1.25rem;
+ height: 1.25rem;
+ cursor: pointer;
+}
+
+/* Crawler Section */
+.crawler-section {
+ background: var(--color-bg-tertiary);
+ padding: var(--spacing-md);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+}
+
+.crawler-settings {
+ margin-top: var(--spacing-md);
+ padding: var(--spacing-md);
+ background: var(--color-bg-secondary);
+ border-radius: var(--radius-md);
+ border: 1px dashed var(--color-border);
+}
+
+/* Range Slider */
+.form-range {
+ width: 100%;
+ height: 6px;
+ background: var(--color-bg-tertiary);
+}
+
+.form-range::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ background: var(--gradient-primary);
+ border-radius: 50%;
+ cursor: pointer;
+ border: none;
+ box-shadow: var(--shadow-sm);
+ transition: all var(--transition-fast);
+}
+
+.range-value {
+ display: inline-block;
+ margin-left: var(--spacing-sm);
+ padding: 0.25rem 0.75rem;
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-sm);
+ font-weight: 600;
+ font-size: 0.875rem;
+ color: var(--color-accent-primary);
+}
+
+/* ===================================
+ BUTTONS
+ =================================== */
+.btn {
+ padding: 0.875rem 1.75rem;
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: var(--font-primary);
+ font-size: 0.9375rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-xs);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-sm {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+}
+
+.btn-primary {
+ background: var(--gradient-primary);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-primary:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-glow), var(--shadow-md);
+}
+
+.btn-primary:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.btn-success {
+ background: var(--gradient-success);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-success:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 0 20px rgba(16, 185, 129, 0.3), var(--shadow-md);
+}
+
+.btn-danger {
+ background: var(--gradient-danger);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-danger:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 0 20px rgba(239, 68, 68, 0.3), var(--shadow-md);
+}
+
+.btn-warning {
+ background: var(--gradient-warning);
+ color: white;
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-warning:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 0 20px rgba(245, 158, 11, 0.3), var(--shadow-md);
+}
+
+.btn-secondary {
+ background: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border);
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: var(--color-bg-glass);
+ border-color: var(--color-border-hover);
+}
+
+/* Button Group */
+.btn-group {
+ display: flex;
+ gap: var(--spacing-md);
+ flex-wrap: wrap;
+}
+
+/* ===================================
+ STATISTICS CARDS
+ =================================== */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-xl);
+}
+
+.stat-card {
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-lg);
+ transition: all var(--transition-base);
+}
+
+.stat-card:hover {
+ border-color: var(--color-border-hover);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.stat-card-large {
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: var(--spacing-xl);
+ text-align: center;
+}
+
+.stat-label {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--color-text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: var(--spacing-xs);
+}
+
+.stat-value {
+ font-size: 2rem;
+ font-weight: 800;
+ color: var(--color-text-primary);
+ line-height: 1;
+}
+
+.stat-value.success {
+ color: var(--color-accent-success);
+}
+
+.stat-value.danger {
+ color: var(--color-accent-danger);
+}
+
+.stat-value.warning {
+ color: var(--color-accent-warning);
+}
+
+.stat-value.info {
+ color: var(--color-accent-info);
+}
+
+/* ===================================
+ CHARTS & VISUALIZATIONS
+ =================================== */
+.chart-container {
+ position: relative;
+ height: 300px;
+ margin-top: var(--spacing-lg);
+ padding: var(--spacing-md);
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+}
+
+/* ===================================
+ REQUEST HISTORY TABLE
+ =================================== */
+.request-history-container {
+ max-height: 400px;
+ overflow-y: auto;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+}
+
+.request-history-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.875rem;
+}
+
+.request-history-table thead {
+ position: sticky;
+ top: 0;
+ background: var(--color-bg-tertiary);
+ z-index: 10;
+}
+
+.request-history-table th {
+ padding: var(--spacing-md);
+ text-align: left;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-size: 0.75rem;
+ border-bottom: 2px solid var(--color-border);
+}
+
+.request-history-table td {
+ padding: var(--spacing-sm) var(--spacing-md);
+ border-bottom: 1px solid var(--color-border);
+ color: var(--color-text-primary);
+}
+
+.request-history-table tr.success-row {
+ background: rgba(16, 185, 129, 0.05);
+}
+
+.request-history-table tr.error-row {
+ background: rgba(239, 68, 68, 0.05);
+}
+
+.request-history-table tr:hover {
+ background: var(--color-bg-glass);
+}
+
+.url-cell {
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.status-code {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ border-radius: var(--radius-sm);
+ font-weight: 600;
+ font-size: 0.75rem;
+}
+
+.status-code.success {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-accent-success);
+}
+
+.status-code.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-accent-danger);
+}
+
+/* ===================================
+ STATUS INDICATORS
+ =================================== */
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ padding: 0.375rem 0.875rem;
+ border-radius: var(--radius-lg);
+ font-size: 0.875rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.status-badge::before {
+ content: '';
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ animation: pulse 2s infinite;
+}
+
+.status-idle {
+ background: rgba(148, 163, 184, 0.1);
+ color: var(--color-text-tertiary);
+}
+
+.status-idle::before {
+ background: var(--color-text-tertiary);
+}
+
+.status-running {
+ background: rgba(16, 185, 129, 0.1);
+ color: var(--color-accent-success);
+}
+
+.status-running::before {
+ background: var(--color-accent-success);
+}
+
+.status-paused {
+ background: rgba(245, 158, 11, 0.1);
+ color: var(--color-accent-warning);
+}
+
+.status-paused::before {
+ background: var(--color-accent-warning);
+}
+
+.status-error {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--color-accent-danger);
+}
+
+.status-error::before {
+ background: var(--color-accent-danger);
+}
+
+/* ===================================
+ PROGRESS BAR
+ =================================== */
+.progress-container {
+ width: 100%;
+ height: 8px;
+ background: var(--color-bg-tertiary);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ margin-top: var(--spacing-md);
+}
+
+.progress-bar {
+ height: 100%;
+ background: var(--gradient-primary);
+ border-radius: var(--radius-lg);
+ transition: width var(--transition-base);
+ position: relative;
+ overflow: hidden;
+}
+
+.progress-bar::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg,
+ transparent,
+ rgba(255, 255, 255, 0.3),
+ transparent);
+ animation: shimmer 2s infinite;
+}
+
+/* ===================================
+ RESULTS TABLE
+ =================================== */
+.results-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: var(--spacing-lg);
+ font-size: 0.875rem;
+}
+
+.results-table th {
+ background: var(--color-bg-tertiary);
+ padding: var(--spacing-md);
+ text-align: left;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-size: 0.75rem;
+ border-bottom: 2px solid var(--color-border);
+}
+
+.results-table td {
+ padding: var(--spacing-md);
+ border-bottom: 1px solid var(--color-border);
+ color: var(--color-text-primary);
+}
+
+.results-table tr:hover {
+ background: var(--color-bg-tertiary);
+}
+
+.results-table code {
+ font-family: var(--font-mono);
+ font-size: 0.8125rem;
+ color: var(--color-accent-primary);
+}
+
+/* ===================================
+ ACCORDION
+ =================================== */
+.accordion {
+ margin-bottom: var(--spacing-md);
+}
+
+.accordion-header {
+ width: 100%;
+ padding: var(--spacing-md);
+ background: var(--color-bg-tertiary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ color: var(--color-text-primary);
+ font-family: var(--font-primary);
+ font-size: 0.9375rem;
+ font-weight: 600;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ transition: all var(--transition-base);
+}
+
+.accordion-header:hover {
+ background: var(--color-bg-glass);
+ border-color: var(--color-border-hover);
+}
+
+.accordion-icon {
+ transition: transform var(--transition-base);
+}
+
+.accordion-header.active .accordion-icon {
+ transform: rotate(180deg);
+}
+
+.accordion-content {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height var(--transition-slow);
+}
+
+.accordion-content.active {
+ max-height: 1000px;
+ padding-top: var(--spacing-md);
+}
+
+/* ===================================
+ ANIMATIONS
+ =================================== */
+@keyframes pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%);
+ }
+
+ 100% {
+ transform: translateX(100%);
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fade-in {
+ animation: fadeIn var(--transition-base) ease-out;
+}
+
+/* ===================================
+ RESPONSIVE DESIGN
+ =================================== */
+@media (max-width: 1024px) {
+ .main-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--spacing-md);
+ }
+
+ .preset-controls {
+ flex-direction: column;
+ }
+}
+
+@media (max-width: 640px) {
+ .container {
+ padding: var(--spacing-md);
+ }
+
+ .header h1 {
+ font-size: 2rem;
+ }
+
+ .header p {
+ font-size: 1rem;
+ }
+
+ .panel {
+ padding: var(--spacing-md);
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .btn-group {
+ flex-direction: column;
+ }
+
+ .btn {
+ width: 100%;
+ }
+
+ .request-history-table {
+ font-size: 0.75rem;
+ }
+
+ .request-history-table th,
+ .request-history-table td {
+ padding: var(--spacing-xs);
+ }
+}
+
+/* ===================================
+ UTILITY CLASSES
+ =================================== */
+.text-center {
+ text-align: center;
+}
+
+.mt-1 {
+ margin-top: var(--spacing-xs);
+}
+
+.mt-2 {
+ margin-top: var(--spacing-sm);
+}
+
+.mt-3 {
+ margin-top: var(--spacing-md);
+}
+
+.mt-4 {
+ margin-top: var(--spacing-lg);
+}
+
+.mb-1 {
+ margin-bottom: var(--spacing-xs);
+}
+
+.mb-2 {
+ margin-bottom: var(--spacing-sm);
+}
+
+.mb-3 {
+ margin-bottom: var(--spacing-md);
+}
+
+.mb-4 {
+ margin-bottom: var(--spacing-lg);
+}
+
+.hidden {
+ display: none;
+}
+
+.full-width {
+ grid-column: 1 / -1;
+}
\ No newline at end of file