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 + + + + + + +
+ +
+
+

+ BCT Logo + Beyond Cloud Technology - Website Stress Test +

+

Simulate realistic traffic patterns to test your production websites

+ + + + + Watch on YouTube @beyondcloudtechnology + +
+
+ +
+
+ + +
+
+
⚔
+

Quick Start

+
+
+ + +
+
+ Keyboard Shortcuts: S = Start | P = Pause/Resume | X = Stop +
+
+ + +
+ +
+
+
āš™ļø
+

Configuration

+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + Randomly navigate through website links like real users +
+ + + + + +
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + Delay between requests per user +
+
+
+
+
+ + +
+
+
šŸŽ®
+

Control Panel

+
+ + +
+ Idle +
+ + +
+
+
+ + +
+ + + +
+ + +
+
+
Elapsed Time
+
0s
+
+
+
Remaining
+
0s
+
+
+
+
+ + +
+
+
šŸ“Š
+

Live Statistics

+
+ +
+
+
Active Users
+
0
+
+
+
Total Requests
+
0
+
+
+
Requests/Sec
+
0
+
+
+
Success Rate
+
0%
+
+
+
Failed Requests
+
0
+
+
+
Avg Response Time
+
0ms
+
+
+ + +
+

Response Time Percentiles

+
+
+
P50 (Median)
+
0ms
+
+
+
P95
+
0ms
+
+
+
P99
+
0ms
+
+
+
+ + +
+

Error Breakdown

+
+
+
4xx Errors
+
0
+
+
+
5xx Errors
+
0
+
+
+
Timeout Errors
+
0
+
+
+
Network Errors
+
0
+
+
+
+ + +
+

Bandwidth Usage

+
+
Total Bandwidth
+
0 B
+
+
+ + +
+
+ +
+
+ +
+
+ + +
+ +
+
+ + +
+
+
šŸ“
+

Request History (Last 100)

+
+ +
+ + + + + + + + + + + + +
TimeURLStatusResponse Time
+
+
+ + + +
+ + + + + + + + \ 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