From dbc8aeeab9622a5c3e22b293f737d84c8e2a9bc5 Mon Sep 17 00:00:00 2001 From: DeNNiiInc Date: Sat, 27 Dec 2025 16:20:14 +1100 Subject: [PATCH] Add deployment scripts and update proxy config --- .gitignore | 3 + PROXMOX_DEPLOY_TEMPLATE.md | 211 ++++++++ auto-sync.sh | 35 ++ deploy-config.example.json | 9 + script.js | 957 +++++++++++++++++++++---------------- setup-nginx.sh | 52 ++ setup-server.sh | 56 +++ start-deployment.ps1 | 73 +++ 8 files changed, 982 insertions(+), 414 deletions(-) create mode 100644 .gitignore create mode 100644 PROXMOX_DEPLOY_TEMPLATE.md create mode 100644 auto-sync.sh create mode 100644 deploy-config.example.json create mode 100644 setup-nginx.sh create mode 100644 setup-server.sh create mode 100644 start-deployment.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0518e9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +deploy-config.json +.env diff --git a/PROXMOX_DEPLOY_TEMPLATE.md b/PROXMOX_DEPLOY_TEMPLATE.md new file mode 100644 index 0000000..5d21676 --- /dev/null +++ b/PROXMOX_DEPLOY_TEMPLATE.md @@ -0,0 +1,211 @@ +# πŸš€ Proxmox Deployment Template (TurnKey Node.js) + +**Use this guide to deploy ANY Node.js application to a TurnKey Linux LXC Container.** + +--- + +## πŸ“‹ Prerequisites + +1. **Project**: A Node.js application (Express, Next.js, etc.) in a Git repository. +2. **Server**: A Proxmox TurnKey Node.js Container. +3. **Access**: Root SSH password for the container. +4. **Domain (Optional)**: If using Cloudflare Tunnel. + +--- + +## πŸ› οΈ Step 1: Prepare Your Project + +Ensure your project is ready for production: + +1. **Port Configuration**: Ensure your app listens on a configurable port or a fixed internal port (e.g., `4001`). + ```javascript + // server.js + const PORT = process.env.PORT || 4001; + app.listen(PORT, ...); + ``` + +2. **Git Ignore**: Ensure `node_modules` and config files with secrets are ignored. + ```gitignore + node_modules/ + .env + config.json + ``` + +--- + +## πŸ–₯️ Step 2: One-Time Server Setup + +SSH into your new container: +```bash +ssh root@ +``` + +Run these commands to prepare the environment: + +### 1. Install Essentials +```bash +apt-get update && apt-get install -y git +``` + +### 2. Prepare Directory +```bash +# Standard web directory +mkdir -p /var/www/ +cd /var/www/ + +# Clone your repo (Use Basic Auth with Token if private) +# Format: https://:@github.com//.git +git clone . + +# Install dependencies +npm install +``` + +### 3. Setup Permissions +```bash +# Give ownership to www-data (Nginx user) +chown -R www-data:www-data /var/www/ +``` + +--- + +## βš™οΈ Step 3: Application Configuration + +### 1. Systemd Service +Create a service file to keep your app running. + +Create `/etc/systemd/system/.service`: +```ini +[Unit] +Description= Service +After=network.target + +[Service] +Type=simple +User=root +# OR use 'www-data' if app doesn't need root ports +# User=www-data +WorkingDirectory=/var/www/ +ExecStart=/usr/local/bin/node server.js +Restart=always +Environment=NODE_ENV=production +Environment=PORT=4001 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: +```bash +systemctl daemon-reload +systemctl enable +systemctl start +``` + +### 2. Nginx Reverse Proxy +Configure Nginx to forward port 80 to your app (Port 4001). + +Create `/etc/nginx/sites-available/`: +```nginx +server { + listen 80; + server_name _; + + root /var/www/; + index index.html; + + # Serve static files (Optional) + location / { + try_files $uri $uri/ =404; + } + + # Proxy API/Dynamic requests + location /api { + proxy_pass http://localhost:4001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +Enable site: +```bash +# Remove defaults +rm -f /etc/nginx/sites-enabled/default +rm -f /etc/nginx/sites-enabled/nodejs + +# Link new site +ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ + +# Reload +nginx -t && systemctl reload nginx +``` + +--- + +## ☁️ Step 4: Cloudflare Tunnel (Secure Access) + +Expose your app securely without opening router ports. + +### 1. Install Cloudflared +```bash +# Add Key +mkdir -p --mode=0755 /usr/share/keyrings +curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null + +# Add Repo +echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list + +# Install +apt-get update && apt-get install -y cloudflared +``` + +### 2. Create Tunnel +```bash +cloudflared tunnel login +cloudflared tunnel create +# Follow on-screen instructions to map domain -> http://localhost:4001 +``` + +--- + +## πŸ”„ Step 5: Automated Updates (PowerShell) + +Create a script `deploy-remote.ps1` in your project root to automate updates. + +**Pre-requisite**: Create `deploy-config.json` (Add to .gitignore!): +```json +{ + "host": "", + "username": "root", + "password": "", + "remotePath": "/var/www/" +} +``` + +**Script `deploy-remote.ps1`**: +```powershell +# Reads config and updates remote server +$Config = Get-Content "deploy-config.json" | ConvertFrom-Json +$User = $Config.username; $HostName = $Config.host; $Pass = $Config.password +$RemotePath = $Config.remotePath + +# Commands to run remotely +$Cmds = " + cd $RemotePath + echo '⬇️ Pulling code...' + git pull + echo 'πŸ“¦ Installing deps...' + npm install + echo 'πŸš€ Restarting service...' + systemctl restart + systemctl status --no-pager +" + +echo y | plink -ssh -t -pw $Pass "$User@$HostName" $Cmds +``` + +**Usage**: Just run `./deploy-remote.ps1` to deploy! diff --git a/auto-sync.sh b/auto-sync.sh new file mode 100644 index 0000000..0ecf02b --- /dev/null +++ b/auto-sync.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# auto-sync.sh - Run by Cron every 5 minutes + +APP_DIR="/var/www/website-stress-test" +APP_NAME="website-stress-test" + +cd "$APP_DIR" || exit + +echo "[$(date)] Checking for updates..." + +# Fetch latest changes +git remote update + +# Check if we are behind +UPSTREAM=${1:-'@{u}'} +LOCAL=$(git rev-parse @) +REMOTE=$(git rev-parse "$UPSTREAM") +BASE=$(git merge-base @ "$UPSTREAM") + +if [ "$LOCAL" = "$REMOTE" ]; then + echo "Up-to-date." +elif [ "$LOCAL" = "$BASE" ]; then + echo "Update available using git pull." + git pull + echo "Installing dependencies..." + npm install + echo "Restarting PM2 process..." + pm2 restart "$APP_NAME" + echo "βœ… Updated and restarted." +elif [ "$REMOTE" = "$BASE" ]; then + echo "Need to push" +else + echo "Diverged" +fi diff --git a/deploy-config.example.json b/deploy-config.example.json new file mode 100644 index 0000000..a900523 --- /dev/null +++ b/deploy-config.example.json @@ -0,0 +1,9 @@ +{ + "host": "YOUR_SERVER_IP", + "username": "root", + "password": "YOUR_SSH_PASSWORD", + "remotePath": "/var/www/website-stress-test", + "repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git", + "githubToken": "YOUR_GITHUB_TOKEN", + "appName": "website-stress-test" +} diff --git a/script.js b/script.js index 1db5049..70b0c1b 100644 --- a/script.js +++ b/script.js @@ -15,15 +15,20 @@ class WebsiteCrawler { extractLinks(html, baseUrl) { const links = []; const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const anchorTags = doc.querySelectorAll('a[href]'); + const doc = parser.parseFromString(html, "text/html"); + const anchorTags = doc.querySelectorAll("a[href]"); const baseUrlObj = new URL(baseUrl); - anchorTags.forEach(anchor => { + anchorTags.forEach((anchor) => { try { - const href = anchor.getAttribute('href'); - if (!href || href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:')) { + const href = anchor.getAttribute("href"); + if ( + !href || + href.startsWith("#") || + href.startsWith("javascript:") || + href.startsWith("mailto:") + ) { return; } @@ -50,7 +55,7 @@ class WebsiteCrawler { // Add new links to queue (limit per page) const linksToAdd = links.slice(0, config.linksPerPage || 10); - linksToAdd.forEach(link => { + linksToAdd.forEach((link) => { if (!this.visitedUrls.has(link) && this.urlQueue.length < 100) { this.urlQueue.push(link); } @@ -85,18 +90,18 @@ function calculatePercentile(arr, percentile) { } 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'; + 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'; + if (bytes === 0) return "0 B"; const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; + 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]; + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; } // =================================== @@ -105,25 +110,30 @@ function formatBytes(bytes) { class StressTestingTool { constructor() { this.config = { - targetUrl: '', + targetUrl: "", userCount: 100, duration: 60, - trafficPattern: 'steady', - httpMethod: 'GET', + trafficPattern: "steady", + httpMethod: "GET", customHeaders: {}, requestBody: null, thinkTime: 1000, - proxyUrl: 'http://localhost:3000', + proxyUrl: + window.location.protocol === "file:" || + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" + ? "http://localhost:3000" + : "/proxy", // Crawler settings crawlerEnabled: false, crawlDepth: 2, linksPerPage: 10, - stayOnDomain: true + stayOnDomain: true, }; this.state = { - status: 'idle', + status: "idle", startTime: null, pauseTime: null, elapsedTime: 0, @@ -141,10 +151,10 @@ class StressTestingTool { // Enhanced metrics errorsByCategory: { - '4xx': 0, - '5xx': 0, - 'timeout': 0, - 'network': 0 + "4xx": 0, + "5xx": 0, + timeout: 0, + network: 0, }, totalBytesSent: 0, totalBytesReceived: 0, @@ -154,23 +164,23 @@ class StressTestingTool { percentiles: { p50: 0, p95: 0, - p99: 0 - } + p99: 0, + }, }; this.crawler = new WebsiteCrawler(); this.charts = { rps: null, responseTime: null, - userError: 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' } + 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(); @@ -188,169 +198,186 @@ class StressTestingTool { 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'), + 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'), + 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'), + 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'), + 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'), + 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'), + requestHistoryBody: document.getElementById("requestHistoryBody"), // Results - resultsPanel: document.getElementById('resultsPanel'), - resultsTableBody: document.getElementById('resultsTableBody'), - exportJsonBtn: document.getElementById('exportJsonBtn'), - exportCsvBtn: document.getElementById('exportCsvBtn'), + 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'), + advancedToggle: document.getElementById("advancedToggle"), + advancedContent: document.getElementById("advancedContent"), // Theme & presets - themeToggle: document.getElementById('themeToggle'), - presetSelect: document.getElementById('presetSelect'), - saveConfigBtn: document.getElementById('saveConfigBtn') + themeToggle: document.getElementById("themeToggle"), + presetSelect: document.getElementById("presetSelect"), + saveConfigBtn: document.getElementById("saveConfigBtn"), }; } attachEventListeners() { // Range inputs - this.elements.userCount.addEventListener('input', (e) => { + this.elements.userCount.addEventListener("input", (e) => { this.elements.userCountValue.textContent = e.target.value; }); - this.elements.duration.addEventListener('input', (e) => { + this.elements.duration.addEventListener("input", (e) => { this.elements.durationValue.textContent = e.target.value; }); - this.elements.thinkTime.addEventListener('input', (e) => { + 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.crawlDepth.addEventListener("input", (e) => { this.elements.crawlDepthValue.textContent = e.target.value; }); } if (this.elements.linksPerPage) { - this.elements.linksPerPage.addEventListener('input', (e) => { + 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()); + 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')); + 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'); + 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()); + this.elements.themeToggle.addEventListener("click", () => + this.toggleTheme() + ); } // Preset selector if (this.elements.presetSelect) { - this.elements.presetSelect.addEventListener('change', (e) => this.loadPreset(e.target.value)); + this.elements.presetSelect.addEventListener("change", (e) => + this.loadPreset(e.target.value) + ); } // Save config if (this.elements.saveConfigBtn) { - this.elements.saveConfigBtn.addEventListener('click', () => this.saveConfig()); + this.elements.saveConfigBtn.addEventListener("click", () => + this.saveConfig() + ); } } setupKeyboardShortcuts() { - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { // Don't trigger if user is typing in an input - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") + return; switch (e.key.toLowerCase()) { - case 's': - if (this.state.status === 'idle') this.startTest(); + case "s": + if (this.state.status === "idle") this.startTest(); break; - case 'p': - if (this.state.status === 'running' || this.state.status === 'paused') this.pauseTest(); + 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(); + 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); + 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); + 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)'; + 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 => { + Object.values(this.charts).forEach((chart) => { if (chart) { chart.options.scales.x.ticks.color = textColor; chart.options.scales.x.grid.color = gridColor; @@ -359,27 +386,27 @@ class StressTestingTool { if (chart.options.scales.y1) { chart.options.scales.y1.ticks.color = textColor; } - chart.update('none'); + chart.update("none"); } }); } loadSavedConfigs() { - const saved = localStorage.getItem('stressTestConfigs'); + 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'); + 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); + console.error("Failed to load saved configs:", e); } } } @@ -388,9 +415,11 @@ class StressTestingTool { if (!presetName) return; let config; - if (presetName.startsWith('saved_')) { - const saved = JSON.parse(localStorage.getItem('stressTestConfigs') || '{}'); - config = saved[presetName.replace('saved_', '')]; + if (presetName.startsWith("saved_")) { + const saved = JSON.parse( + localStorage.getItem("stressTestConfigs") || "{}" + ); + config = saved[presetName.replace("saved_", "")]; } else { config = this.presets[presetName]; } @@ -405,159 +434,170 @@ class StressTestingTool { } saveConfig() { - const name = prompt('Enter a name for this configuration:'); + 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 + targetUrl: this.elements.targetUrl.value, }; - const saved = JSON.parse(localStorage.getItem('stressTestConfigs') || '{}'); + const saved = JSON.parse(localStorage.getItem("stressTestConfigs") || "{}"); saved[name] = config; - localStorage.setItem('stressTestConfigs', JSON.stringify(saved)); + 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 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 - } + display: false, + }, }, scales: { x: { grid: { - color: gridColor + color: gridColor, }, ticks: { - color: textColor - } + color: textColor, + }, }, y: { grid: { - color: gridColor + color: gridColor, }, ticks: { - color: textColor + color: textColor, }, - beginAtZero: true - } - } + beginAtZero: true, + }, + }, }; // RPS Chart - const rpsCtx = document.getElementById('rpsChart').getContext('2d'); + 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', + type: "line", data: { labels: [], datasets: [ { - label: 'Active Users', + label: "Requests per Second", data: [], - borderColor: '#3b82f6', - backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderColor: "#6366f1", + backgroundColor: "rgba(99, 102, 241, 0.1)", borderWidth: 2, fill: true, tension: 0.4, - yAxisID: 'y' + }, + ], + }, + 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 (%)', + label: "Error Rate (%)", data: [], - borderColor: '#ef4444', - backgroundColor: 'rgba(239, 68, 68, 0.2)', + borderColor: "#ef4444", + backgroundColor: "rgba(239, 68, 68, 0.2)", borderWidth: 2, fill: true, tension: 0.4, - yAxisID: 'y1' - } - ] + yAxisID: "y1", + }, + ], }, options: { responsive: true, maintainAspectRatio: false, interaction: { - mode: 'index', - intersect: false + mode: "index", + intersect: false, }, plugins: { legend: { @@ -566,74 +606,74 @@ class StressTestingTool { color: textColor, font: { size: 12, - weight: 600 - } - } + weight: 600, + }, + }, }, title: { display: true, - text: 'User Load vs Error Rate', + text: "User Load vs Error Rate", color: textColor, font: { size: 14, - weight: 600 - } - } + weight: 600, + }, + }, }, scales: { x: { grid: { - color: gridColor + color: gridColor, }, ticks: { - color: textColor - } + color: textColor, + }, }, y: { - type: 'linear', + type: "linear", display: true, - position: 'left', + position: "left", title: { display: true, - text: 'Active Users', - color: '#3b82f6', + text: "Active Users", + color: "#3b82f6", font: { size: 12, - weight: 600 - } + weight: 600, + }, }, grid: { - color: gridColor + 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 + color: textColor, }, beginAtZero: true, - max: 100 - } - } - } + }, + 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, + }, + }, + }, }); } @@ -644,7 +684,7 @@ class StressTestingTool { this.gatherConfig(); this.resetState(); - this.updateStatus('running'); + this.updateStatus("running"); this.state.startTime = Date.now(); // Update UI @@ -657,26 +697,29 @@ class StressTestingTool { // Start update intervals this.state.updateInterval = setInterval(() => this.updateStatistics(), 100); - this.state.chartUpdateInterval = setInterval(() => this.updateCharts(), 1000); + this.state.chartUpdateInterval = setInterval( + () => this.updateCharts(), + 1000 + ); } pauseTest() { - if (this.state.status === 'running') { - this.updateStatus('paused'); + 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'); + 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'; + this.elements.pauseBtn.textContent = "⏸️ Pause"; } } stopTest() { - this.updateStatus('stopped'); + this.updateStatus("stopped"); this.stopWorkers(); clearInterval(this.state.updateInterval); clearInterval(this.state.chartUpdateInterval); @@ -685,7 +728,7 @@ class StressTestingTool { this.elements.startBtn.disabled = false; this.elements.pauseBtn.disabled = true; this.elements.stopBtn.disabled = true; - this.elements.pauseBtn.textContent = '⏸️ Pause'; + this.elements.pauseBtn.textContent = "⏸️ Pause"; // Calculate final percentiles this.calculatePercentiles(); @@ -697,14 +740,14 @@ class StressTestingTool { validateConfig() { const url = this.elements.targetUrl.value.trim(); if (!url) { - alert('Please enter a target URL'); + alert("Please enter a target URL"); return false; } try { new URL(url); } catch (e) { - alert('Please enter a valid URL'); + alert("Please enter a valid URL"); return false; } @@ -713,7 +756,7 @@ class StressTestingTool { try { JSON.parse(headersText); } catch (e) { - alert('Custom headers must be valid JSON'); + alert("Custom headers must be valid JSON"); return false; } } @@ -723,7 +766,7 @@ class StressTestingTool { try { JSON.parse(bodyText); } catch (e) { - alert('Request body must be valid JSON'); + alert("Request body must be valid JSON"); return false; } } @@ -749,7 +792,9 @@ class StressTestingTool { 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); + this.config.linksPerPage = parseInt( + this.elements.linksPerPage?.value || 10 + ); } } @@ -764,7 +809,12 @@ class StressTestingTool { this.state.workers = []; this.state.userErrorData = []; this.state.errorThreshold = null; - this.state.errorsByCategory = { '4xx': 0, '5xx': 0, 'timeout': 0, 'network': 0 }; + this.state.errorsByCategory = { + "4xx": 0, + "5xx": 0, + timeout: 0, + network: 0, + }; this.state.totalBytesSent = 0; this.state.totalBytesReceived = 0; this.state.requestHistory = []; @@ -784,33 +834,33 @@ class StressTestingTool { 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'); + 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 = ''; + this.elements.requestHistoryBody.innerHTML = ""; } // Hide results panel - this.elements.resultsPanel.style.display = 'none'; + this.elements.resultsPanel.style.display = "none"; } startWorkers() { const pattern = this.config.trafficPattern; switch (pattern) { - case 'steady': + case "steady": this.startSteadyLoad(); break; - case 'burst': + case "burst": this.startBurstLoad(); break; - case 'rampup': + case "rampup": this.startRampUpLoad(); break; - case 'random': + case "random": this.startRandomLoad(); break; } @@ -821,7 +871,7 @@ class StressTestingTool { for (let i = 0; i < this.config.userCount; i++) { setTimeout(() => { - if (this.state.status === 'running') { + if (this.state.status === "running") { this.createWorker(i); } }, i * delayBetweenUsers); @@ -834,7 +884,7 @@ class StressTestingTool { for (let burst = 0; burst < 5; burst++) { setTimeout(() => { - if (this.state.status === 'running') { + if (this.state.status === "running") { for (let i = 0; i < burstSize; i++) { this.createWorker(burst * burstSize + i); } @@ -849,7 +899,7 @@ class StressTestingTool { for (let i = 0; i < this.config.userCount; i++) { setTimeout(() => { - if (this.state.status === 'running') { + if (this.state.status === "running") { this.createWorker(i); } }, i * timePerUser); @@ -862,7 +912,7 @@ class StressTestingTool { for (let i = 0; i < this.config.userCount; i++) { const randomDelay = Math.random() * maxDelay; setTimeout(() => { - if (this.state.status === 'running') { + if (this.state.status === "running") { this.createWorker(i); } }, randomDelay); @@ -875,7 +925,7 @@ class StressTestingTool { active: true, requestCount: 0, currentUrl: this.config.targetUrl, - crawlDepth: 0 + crawlDepth: 0, }; this.state.workers.push(worker); @@ -884,9 +934,13 @@ class StressTestingTool { } async runWorker(worker) { - const endTime = this.state.startTime + (this.config.duration * 1000); + const endTime = this.state.startTime + this.config.duration * 1000; - while (worker.active && this.state.status === 'running' && Date.now() < endTime) { + while ( + worker.active && + this.state.status === "running" && + Date.now() < endTime + ) { await this.makeRequest(worker); // Think time @@ -900,7 +954,7 @@ class StressTestingTool { this.state.activeUsers--; // Check if all workers are done - if (this.state.activeUsers === 0 && this.state.status === 'running') { + if (this.state.activeUsers === 0 && this.state.status === "running") { this.stopTest(); } } @@ -914,7 +968,7 @@ class StressTestingTool { targetUrl: requestUrl, method: this.config.httpMethod, headers: this.config.customHeaders, - body: this.config.requestBody + body: this.config.requestBody, }; // Estimate request size @@ -922,11 +976,11 @@ class StressTestingTool { this.state.totalBytesSent += requestSize; const response = await fetch(this.config.proxyUrl, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, - body: JSON.stringify(proxyPayload) + body: JSON.stringify(proxyPayload), }); const endTime = performance.now(); @@ -942,13 +996,19 @@ class StressTestingTool { this.state.totalBytesReceived += proxyResponse.body.length; } - const isSuccess = proxyResponse.success && proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 400; + 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); + const category = categorizeError( + proxyResponse.statusCode, + proxyResponse.error + ); this.state.errorsByCategory[category]++; } @@ -966,18 +1026,26 @@ class StressTestingTool { status: proxyResponse.statusCode, responseTime: Math.round(actualResponseTime), success: isSuccess, - timestamp: new Date().toLocaleTimeString() + 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 ( + 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; @@ -985,7 +1053,7 @@ class StressTestingTool { this.state.totalRequests++; this.state.failedRequests++; this.state.responseTimes.push(responseTime); - this.state.errorsByCategory['network']++; + this.state.errorsByCategory["network"]++; worker.requestCount++; this.addToRequestHistory({ @@ -994,7 +1062,7 @@ class StressTestingTool { responseTime: Math.round(responseTime), success: false, timestamp: new Date().toLocaleTimeString(), - error: error.message + error: error.message, }); } } @@ -1009,42 +1077,57 @@ class StressTestingTool { // Update UI table if (this.elements.requestHistoryBody) { - const row = document.createElement('tr'); - row.className = request.success ? 'success-row' : 'error-row'; + const row = document.createElement("tr"); + row.className = request.success ? "success-row" : "error-row"; row.innerHTML = ` ${request.timestamp} - ${this.truncateUrl(request.url)} - ${request.status} + ${this.truncateUrl( + request.url + )} + ${request.status} ${request.responseTime}ms `; - this.elements.requestHistoryBody.insertBefore(row, this.elements.requestHistoryBody.firstChild); + 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); + this.elements.requestHistoryBody.removeChild( + this.elements.requestHistoryBody.lastChild + ); } } } truncateUrl(url) { if (url.length > 50) { - return url.substring(0, 47) + '...'; + return url.substring(0, 47) + "..."; } return url; } stopWorkers() { - this.state.workers.forEach(worker => { + 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)); + 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) + ); } } @@ -1061,23 +1144,34 @@ class StressTestingTool { // 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(); + 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; + 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; + 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; + 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 @@ -1092,14 +1186,17 @@ class StressTestingTool { } 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']; + 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; + const totalBytes = + this.state.totalBytesSent + this.state.totalBytesReceived; this.elements.totalBandwidth.textContent = formatBytes(totalBytes); } } @@ -1109,15 +1206,20 @@ class StressTestingTool { 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; + 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; + 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`); @@ -1128,7 +1230,7 @@ class StressTestingTool { this.charts.rps.data.datasets[0].data.shift(); } - this.charts.rps.update('none'); + this.charts.rps.update("none"); // Update Response Time chart this.charts.responseTime.data.labels.push(`${elapsed}s`); @@ -1139,32 +1241,42 @@ class StressTestingTool { this.charts.responseTime.data.datasets[0].data.shift(); } - this.charts.responseTime.update('none'); + 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; + 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)); + 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 + failedRequests: this.state.failedRequests, }); // Detect error threshold - if (this.state.errorThreshold === null && this.state.failedRequests > 0 && this.state.activeUsers > 0) { + 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) + errorRate: parseFloat(currentErrorRate), }; } @@ -1174,45 +1286,45 @@ class StressTestingTool { this.charts.userError.data.datasets[1].data.shift(); } - this.charts.userError.update('none'); + this.charts.userError.update("none"); } updateStatus(status) { this.state.status = status; const badge = this.elements.statusBadge; - badge.className = 'status-badge'; + badge.className = "status-badge"; switch (status) { - case 'idle': - badge.classList.add('status-idle'); - badge.textContent = 'Idle'; + case "idle": + badge.classList.add("status-idle"); + badge.textContent = "Idle"; break; - case 'running': - badge.classList.add('status-running'); - badge.textContent = 'Running'; + case "running": + badge.classList.add("status-running"); + badge.textContent = "Running"; break; - case 'paused': - badge.classList.add('status-paused'); - badge.textContent = 'Paused'; + case "paused": + badge.classList.add("status-paused"); + badge.textContent = "Paused"; break; - case 'stopped': - badge.classList.add('status-idle'); - badge.textContent = 'Completed'; + case "stopped": + badge.classList.add("status-idle"); + badge.textContent = "Completed"; break; } } displayResults() { - this.elements.resultsPanel.style.display = 'block'; + this.elements.resultsPanel.style.display = "block"; const results = this.calculateResults(); const tbody = this.elements.resultsTableBody; - tbody.innerHTML = ''; + tbody.innerHTML = ""; // Populate results table Object.entries(results).forEach(([key, value]) => { - const row = document.createElement('tr'); + const row = document.createElement("tr"); row.innerHTML = ` ${key} ${value} @@ -1221,62 +1333,77 @@ class StressTestingTool { }); // Scroll to results - this.elements.resultsPanel.scrollIntoView({ behavior: 'smooth' }); + 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 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 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 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 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 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 + "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' + : "No errors detected", }; if (this.config.crawlerEnabled) { - results['Unique URLs Visited'] = this.crawler.visitedUrls.size; + results["Unique URLs Visited"] = this.crawler.visitedUrls.size; } return results; @@ -1284,32 +1411,34 @@ class StressTestingTool { exportResults(format) { const results = this.calculateResults(); - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - if (format === 'json') { + if (format === "json") { const data = { config: this.config, results: results, requestHistory: this.state.requestHistory.slice(0, 100), - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + 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'; + } 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' }); + 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'); + const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); @@ -1319,11 +1448,11 @@ class StressTestingTool { } sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } } // Initialize the application -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener("DOMContentLoaded", () => { new StressTestingTool(); }); diff --git a/setup-nginx.sh b/setup-nginx.sh new file mode 100644 index 0000000..81393b5 --- /dev/null +++ b/setup-nginx.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# setup-nginx.sh + +APP_NAME="website-stress-test" + +# Install Nginx if not present +if ! command -v nginx &> /dev/null; then + echo "Installing Nginx..." + apt-get update + apt-get install -y nginx +fi + +# Create Config +cat > /etc/nginx/sites-available/$APP_NAME </dev/null; echo "*/5 * * * * $SCRIPT_PATH >> /var/log/app-sync.log 2>&1") | crontab - + +echo "βœ… Setup Complete! Application is running." +pm2 status diff --git a/start-deployment.ps1 b/start-deployment.ps1 new file mode 100644 index 0000000..53ed00d --- /dev/null +++ b/start-deployment.ps1 @@ -0,0 +1,73 @@ +# start-deployment.ps1 +# Automates the deployment by reading config, uploading scripts, and executing setup. + +$ErrorActionPreference = "Stop" + +$ConfigPath = "deploy-config.json" + +if (-not (Test-Path $ConfigPath)) { + Write-Error "Configuration file '$ConfigPath' not found. Please copy 'deploy-config.example.json' to '$ConfigPath' and fill in your details." +} + +$Config = Get-Content $ConfigPath | ConvertFrom-Json + +# Validate Config +$Required = @("host", "username", "password", "remotePath", "repoUrl", "githubToken") +foreach ($Key in $Required) { + if (-not $Config.$Key) { + Write-Error "Missing required config key: $Key" + } +} + +$User = $Config.username +$HostName = $Config.host +$Pass = $Config.password +# Note: Using password directly in script is tricky with standard ssh/scp without key. +# We will check if 'sshpass' or 'plink' is available, or guide user to use keys. +# Since the user specifically mentioned providing credentials, they might expect us to use them. +# The template used 'plink -pw $Pass'. We will stick to that if available, or warn. + +# Check for plink +if (Get-Command "plink.exe" -ErrorAction SilentlyContinue) { + Write-Host "Using plink for connection..." + $UsePlink = $true +} +else { + Write-Warning "plink.exe not found. Falling back to standard scp/ssh. You may be prompted for password multiple times." + $UsePlink = $false +} + +$RemoteTmp = "/tmp" +$SetupScript = "setup-server.sh" +$SyncScript = "auto-sync.sh" + +Write-Host "πŸš€ Starting Deployment to $HostName..." + +# 1. Upload Scripts +Write-Host "Uploading scripts..." +if ($UsePlink) { + echo y | pscp -P 22 -pw $Pass $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript" + echo y | pscp -P 22 -pw $Pass $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript" +} +else { + scp $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript" + scp $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript" +} + +# 2. Execute Setup +Write-Host "Executing setup on remote server..." +$AppDir = $Config.remotePath +$Repo = $Config.repoUrl +$Token = $Config.githubToken + +# Make scripts executable and run setup +$RemoteCmd = "chmod +x $RemoteTmp/$SetupScript $RemoteTmp/$SyncScript; $RemoteTmp/$SetupScript '$Repo' '$AppDir' '$Token'; rm $RemoteTmp/$SetupScript" + +if ($UsePlink) { + echo y | plink -ssh -P 22 -t -pw $Pass "$User@$HostName" $RemoteCmd +} +else { + ssh -t "$User@$HostName" $RemoteCmd +} + +Write-Host "πŸŽ‰ Deployment command sent!"