mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 11:55:59 +00:00
V2 Feature overhaul and improvements
This commit is contained in:
200
compare.html
200
compare.html
@@ -3,105 +3,145 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Comparison - Web Page Performance Test</title>
|
||||
<title>Visual Comparison - Web Page Performance Test</title>
|
||||
<link rel="icon" type="image/png" href="Logo.png">
|
||||
<link rel="stylesheet" href="styles.css?v=3.0">
|
||||
<link rel="stylesheet" href="styles.css?v=2.2">
|
||||
<style>
|
||||
.comparison-container {
|
||||
max-width: 1600px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.metric-diff {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.diff-better { color: var(--color-accent-success); font-weight: bold; }
|
||||
.diff-worse { color: var(--color-accent-error); font-weight: bold; }
|
||||
.diff-neutral { color: var(--color-text-secondary); }
|
||||
|
||||
.filmstrip-row {
|
||||
.video-wrapper {
|
||||
margin-top: 2rem;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
background: #000;
|
||||
}
|
||||
video {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.labels {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
padding: 1rem 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filmstrip-img {
|
||||
height: 100px;
|
||||
border: 1px solid var(--color-border);
|
||||
.loading-state {
|
||||
margin-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid rgba(255,255,255,0.1);
|
||||
border-top: 5px solid var(--color-accent-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
@keyframes spin { 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo-container">
|
||||
<img src="Logo.png" alt="Logo" class="logo">
|
||||
<h1>Test Comparison</h1>
|
||||
</div>
|
||||
<a href="/" class="btn-secondary">← Back to Dashboard</a>
|
||||
</header>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">Visual Comparison</h1>
|
||||
<p class="subtitle">Side-by-Side Video Analysis</p>
|
||||
</header>
|
||||
|
||||
<div class="comparison-container">
|
||||
<div id="loading" style="text-align: center; padding: 2rem;">Loading comparison...</div>
|
||||
|
||||
<div id="comparison-content" style="display: none;">
|
||||
<div class="comparison-grid">
|
||||
<!-- Test A -->
|
||||
<div class="test-column" id="col-a">
|
||||
<h2 id="title-a">Test A</h2>
|
||||
<div class="card">
|
||||
<div id="meta-a"></div>
|
||||
<div class="grade-circle" id="grade-a">
|
||||
<span class="grade-letter"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="filmstrip-a" class="filmstrip-row"></div>
|
||||
</div>
|
||||
|
||||
<!-- Test B -->
|
||||
<div class="test-column" id="col-b">
|
||||
<h2 id="title-b">Test B</h2>
|
||||
<div class="card">
|
||||
<div id="meta-b"></div>
|
||||
<div class="grade-circle" id="grade-b">
|
||||
<span class="grade-letter"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="filmstrip-b" class="filmstrip-row"></div>
|
||||
</div>
|
||||
<div class="comparison-container">
|
||||
<div id="loading" class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<h2>Generating Comparison Video...</h2>
|
||||
<p style="color: var(--color-text-tertiary)">This requires processing video frames on the server. Please wait.</p>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Diff Table -->
|
||||
<div class="card" style="margin-top: 2rem;">
|
||||
<h3>Metrics Comparison</h3>
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Test A</th>
|
||||
<th>Test B</th>
|
||||
<th>Difference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="metrics-body"></tbody>
|
||||
</table>
|
||||
<div id="result" style="display: none; width: 100%;">
|
||||
<div class="labels">
|
||||
<div id="label-left">Test A</div>
|
||||
<div id="label-right">Test B</div>
|
||||
</div>
|
||||
<div class="video-wrapper">
|
||||
<video id="comparison-video" controls autoplay loop playsinline>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 2rem;">
|
||||
<a href="/" class="btn-secondary">⬅ Back to Dashboard</a>
|
||||
<a href="#" id="download-link" class="btn-primary" download="comparison.mp4">⬇ Download Video</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error" style="display: none; color: var(--color-accent-danger); text-align: center; margin-top: 2rem;">
|
||||
<h3>Error Generating Comparison</h3>
|
||||
<p id="error-msg"></p>
|
||||
<a href="/" class="btn-secondary" style="margin-top: 1rem;">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="compare.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const test1 = params.get('test1');
|
||||
const test2 = params.get('test2');
|
||||
|
||||
if (!test1 || !test2) {
|
||||
showError('Missing test IDs in URL parameters.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update Labels (Optional: fetch test details to show URLs)
|
||||
// For now just show IDs or generic labels
|
||||
document.getElementById('label-left').textContent = `Test ${test1.substring(0, 8)}...`;
|
||||
document.getElementById('label-right').textContent = `Test ${test2.substring(0, 8)}...`;
|
||||
|
||||
try {
|
||||
// Fetch info to get URLs (optional enhancement)
|
||||
// const [res1, res2] = await Promise.all([fetch('/reports/'+test1+'.json'), fetch('/reports/'+test2+'.json')]);
|
||||
// ... logic to get URLs ...
|
||||
|
||||
const videoSrc = `/api/compare?test1=${test1}&test2=${test2}`;
|
||||
const videoEl = document.getElementById('comparison-video');
|
||||
|
||||
// We set the src. The browser will try to load it.
|
||||
// Since the API generates it on the fly, it might take a moment before the first byte.
|
||||
// However, <video> tag handles buffering.
|
||||
// But if the server takes 10s to RESPOND, the browser might timeout or show error.
|
||||
// Better approach: fetch the blob, then show.
|
||||
|
||||
const response = await fetch(videoSrc);
|
||||
if (!response.ok) throw new Error('Server failed to generate video.');
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
videoEl.src = url;
|
||||
document.getElementById('download-link').href = url;
|
||||
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('result').style.display = 'block';
|
||||
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
function showError(msg) {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('error').style.display = 'block';
|
||||
document.getElementById('error-msg').textContent = msg;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,6 +41,19 @@ fi
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "📦 Installing git..."
|
||||
apt-get update && apt-get install -y git
|
||||
apt-get update && apt-get install -y git
|
||||
fi
|
||||
|
||||
# Install ffmpeg if not present
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
echo "📦 Installing ffmpeg..."
|
||||
apt-get update && apt-get install -y ffmpeg
|
||||
fi
|
||||
|
||||
# Install traceroute if not present
|
||||
if ! command -v traceroute &> /dev/null; then
|
||||
echo "📦 Installing traceroute..."
|
||||
apt-get update && apt-get install -y traceroute
|
||||
fi
|
||||
|
||||
# Create app directory
|
||||
|
||||
20
index.html
20
index.html
@@ -28,6 +28,10 @@
|
||||
</svg>
|
||||
Watch on YouTube @beyondcloudtechnology
|
||||
</a>
|
||||
<!-- Navigation -->
|
||||
<nav style="margin-top: 0.5rem;">
|
||||
<a href="traceroute.html" class="button" style="color:var(--color-text-primary); text-decoration:none; margin-right: 1rem;">🌍 Network Trace</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<!-- Theme toggle or other controls can go here -->
|
||||
@@ -106,6 +110,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Carbon Section -->
|
||||
<div id="carbon-card" class="dashboard-card carbon-card" style="display: none;">
|
||||
<h3>Carbon Footprint</h3>
|
||||
<div class="carbon-grid">
|
||||
<div class="carbon-item">
|
||||
<div class="carbon-label">CO2 per Visit</div>
|
||||
<div class="carbon-value" id="carbon-co2">-</div>
|
||||
</div>
|
||||
<div class="carbon-item">
|
||||
<div class="carbon-label">Eco Rating</div>
|
||||
<div class="carbon-value" id="carbon-rating">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="carbon-badge" class="carbon-badge">-</div>
|
||||
</div>
|
||||
|
||||
<!-- Vitals Section -->
|
||||
<div class="dashboard-card vitals-card">
|
||||
<h3>Web Vitals</h3>
|
||||
|
||||
82
lib/analysis/carbon-calculator.js
Normal file
82
lib/analysis/carbon-calculator.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Carbon Calculator
|
||||
* Based on the Sustainable Web Design model.
|
||||
*/
|
||||
|
||||
// 1 byte = X kWh?
|
||||
// The model: 0.81 kWh / GB for data transfer (2021/2022 estimate)
|
||||
// Carbon intensity: ~442g/kWh (global average)
|
||||
//
|
||||
// Formula:
|
||||
// Energy = (Data Transfer (GB) * 0.81) * 0.75 (system boundary)
|
||||
// Carbon = Energy * 442g
|
||||
|
||||
const SWD_KWH_PER_GB = 0.81;
|
||||
const CARBON_INTENSITY_GLOBAL = 442; // g/kWh
|
||||
|
||||
/**
|
||||
* Checks if a domain is hosted on green infrastructure.
|
||||
* @param {string} domain
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function checkGreenHosting(domain) {
|
||||
try {
|
||||
const res = await fetch(`https://api.thegreenwebfoundation.org/greencheck/${domain}`);
|
||||
const data = await res.json();
|
||||
return data.green || false;
|
||||
} catch (e) {
|
||||
console.warn('Green hosting check failed:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the carbon footprint of a page load.
|
||||
* @param {number} bytes - Total transfer size in bytes.
|
||||
* @param {boolean} isGreen - Whether the host is green.
|
||||
* @returns {object} - { co2: number (grams), rating: string }
|
||||
*/
|
||||
function calculateCarbon(bytes, isGreen) {
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
let energy = gb * SWD_KWH_PER_GB * 0.75; // kWh
|
||||
|
||||
// Carbon intensity adjustment
|
||||
// If green, we assume a lower intensity or offset (often considered 0 or low, but let's say 50g/kWh for production/network overhead)
|
||||
// The standard generally keeps network intensity the same but reduced datacenter intensity.
|
||||
// For simplicity/standard SWD: Green hosting reduces the data center portion.
|
||||
// Let's use simple factor: if green, reduce total by 10-20%?
|
||||
// Actually, SWD v3 splits it:
|
||||
// Consumer device: 52%
|
||||
// Network: 14%
|
||||
// Data Center: 15%
|
||||
// Hardware production: 19%
|
||||
//
|
||||
// Green hosting only affects Data Center (15%).
|
||||
// So if green, we assume 0 emissions for that 15%, reducing total by 15%.
|
||||
|
||||
const intensity = isGreen ? CARBON_INTENSITY_GLOBAL * 0.85 : CARBON_INTENSITY_GLOBAL;
|
||||
|
||||
const co2 = energy * intensity;
|
||||
|
||||
// Rating (Eco-Index style)
|
||||
// < 0.5g -> A+
|
||||
// < 1g -> A
|
||||
// < 2g -> B
|
||||
// < 3g -> C
|
||||
// > 5g -> F
|
||||
let rating = 'F';
|
||||
if(co2 < 0.5) rating = 'A+';
|
||||
else if(co2 < 1.0) rating = 'A';
|
||||
else if(co2 < 2.0) rating = 'B';
|
||||
else if(co2 < 3.0) rating = 'C';
|
||||
else if(co2 < 4.0) rating = 'D';
|
||||
else if(co2 < 5.0) rating = 'E';
|
||||
|
||||
return {
|
||||
co2: parseFloat(co2.toFixed(3)),
|
||||
rating,
|
||||
energy: parseFloat(energy.toFixed(5))
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { checkGreenHosting, calculateCarbon };
|
||||
45
lib/comparison/stitcher.js
Normal file
45
lib/comparison/stitcher.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Stitches two videos side-by-side.
|
||||
* @param {string} video1 Path to first video
|
||||
* @param {string} video2 Path to second video
|
||||
* @param {string} outputPath Path to save output
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function stitchVideos(video1, video2, outputPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!fs.existsSync(video1) || !fs.existsSync(video2)) {
|
||||
return reject(new Error('One or both input videos not found'));
|
||||
}
|
||||
|
||||
ffmpeg()
|
||||
.input(video1)
|
||||
.input(video2)
|
||||
.complexFilter([
|
||||
{
|
||||
filter: 'hstack',
|
||||
options: { inputs: 2 },
|
||||
outputs: 'v'
|
||||
}
|
||||
])
|
||||
.map('v') // Map the video stream from filter
|
||||
// We assume no audio for speed tests usually, or we ignore it
|
||||
.outputOptions([
|
||||
'-c:v libx264',
|
||||
'-crf 23',
|
||||
'-preset fast'
|
||||
])
|
||||
.save(outputPath)
|
||||
.on('end', () => {
|
||||
resolve(outputPath);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
console.error('FFmpeg error:', err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { stitchVideos };
|
||||
67
lib/comparison/video-generator.js
Normal file
67
lib/comparison/video-generator.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
/**
|
||||
* Creates an MP4 video from an array of frame objects.
|
||||
* @param {Array} frames - Array of { data: base64 } objects
|
||||
* @param {number} outputFps - Frames per second
|
||||
* @returns {Promise<string>} - Path to generated video file
|
||||
*/
|
||||
async function createVideoFromFrames(frames, outputFps = 10) {
|
||||
if (!frames || frames.length === 0) throw new Error('No frames provided');
|
||||
|
||||
// Filter out invalid frames if any
|
||||
const validFrames = frames.filter(f => f && f.data);
|
||||
if (validFrames.length === 0) throw new Error('No valid frames found');
|
||||
|
||||
const tempDir = path.join(os.tmpdir(), 'frames-' + uuidv4());
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
try {
|
||||
// Save frames as images
|
||||
for (let i = 0; i < validFrames.length; i++) {
|
||||
const frame = validFrames[i];
|
||||
// Remove header if present
|
||||
const base64Data = frame.data.replace(/^data:image\/\w+;base64,/, "");
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
const filePath = path.join(tempDir, `frame-${i.toString().padStart(5, '0')}.jpg`);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
}
|
||||
|
||||
const outputPath = path.join(os.tmpdir(), `video-${uuidv4()}.mp4`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg()
|
||||
.input(path.join(tempDir, 'frame-%05d.jpg'))
|
||||
.inputFPS(outputFps)
|
||||
.output(outputPath)
|
||||
.outputOptions([
|
||||
'-c:v libx264',
|
||||
'-pix_fmt yuv420p',
|
||||
'-crf 23',
|
||||
'-preset fast',
|
||||
'-movflags +faststart' // Optimize for web playback
|
||||
])
|
||||
.on('end', () => {
|
||||
// Cleanup frames
|
||||
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch(e) {}
|
||||
resolve(outputPath);
|
||||
})
|
||||
.on('error', (err) => {
|
||||
// Cleanup frames
|
||||
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch(e) {}
|
||||
console.error('FFmpeg video generation error:', err);
|
||||
reject(err);
|
||||
})
|
||||
.run();
|
||||
});
|
||||
} catch (e) {
|
||||
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch(err) {}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createVideoFromFrames };
|
||||
81
lib/diagnostics/traceroute.js
Normal file
81
lib/diagnostics/traceroute.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const { exec } = require('child_process');
|
||||
const dns = require('dns');
|
||||
|
||||
/**
|
||||
* Executes a traceroute to the specified host.
|
||||
* @param {string} host - The hostname or IP to trace.
|
||||
* @returns {Promise<string>} - The raw stdout output of the traceroute command.
|
||||
*/
|
||||
function runTraceroute(host) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Basic validation to prevent command injection
|
||||
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
|
||||
return reject(new Error('Invalid hostname format.'));
|
||||
}
|
||||
|
||||
// Resolve domain to ensure it exists before running system command (optional safety)
|
||||
dns.lookup(host, (err) => {
|
||||
if (err) return reject(new Error(`Could not resolve host: ${host}`));
|
||||
|
||||
// Detect platform
|
||||
const isWin = process.platform === 'win32';
|
||||
const command = isWin ? `tracert -d -h 20 ${host}` : `traceroute -n -m 20 ${host}`;
|
||||
|
||||
exec(command, { timeout: 60000 }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// Traceroute might return error code if it doesn't reach dest, but stdout is valuable
|
||||
if (stdout) resolve(stdout);
|
||||
else reject(error || stderr);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const geoip = require('geoip-lite');
|
||||
|
||||
/**
|
||||
* Parses traceroute output and adds geolocation data.
|
||||
* @param {string} output - Raw traceroute output.
|
||||
* @returns {Array} - Array of hop objects with IP, RTT, and Location.
|
||||
*/
|
||||
function parseAndLocate(output) {
|
||||
const lines = output.split('\n');
|
||||
const hops = [];
|
||||
|
||||
// Regex to match lines like: " 1 192.168.1.1 2.5 ms"
|
||||
// Linux: 1 _gateway (10.0.2.2) 0.224 ms 0.180 ms 0.120 ms
|
||||
// Windows: 1 <1 ms <1 ms <1 ms 192.168.1.1
|
||||
const ipRegex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
|
||||
|
||||
lines.forEach(line => {
|
||||
const match = line.match(ipRegex);
|
||||
if (match) {
|
||||
const ip = match[0];
|
||||
const geo = geoip.lookup(ip);
|
||||
|
||||
hops.push({
|
||||
raw: line.trim(),
|
||||
ip: ip,
|
||||
lat: geo ? geo.ll[0] : null,
|
||||
lon: geo ? geo.ll[1] : null,
|
||||
city: geo ? geo.city : null,
|
||||
country: geo ? geo.country : null
|
||||
});
|
||||
}
|
||||
});
|
||||
return hops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes traceroute and returns structured data.
|
||||
*/
|
||||
async function traceAndLocate(host) {
|
||||
const rawOutput = await runTraceroute(host);
|
||||
const hops = parseAndLocate(rawOutput);
|
||||
return { raw: rawOutput, hops };
|
||||
}
|
||||
|
||||
module.exports = { runTraceroute, traceAndLocate };
|
||||
@@ -174,9 +174,37 @@ async function _executeTest(url, options) {
|
||||
},
|
||||
userAgent: lhr.userAgent,
|
||||
isMobile: isMobile,
|
||||
filmstrip: captureFilmstrip ? (lhr.audits['screenshot-thumbnails']?.details?.items || []) : []
|
||||
filmstrip: captureFilmstrip ? (lhr.audits['screenshot-thumbnails']?.details?.items || []) : [],
|
||||
details: {
|
||||
lcpElement: lhr.audits['largest-contentful-paint-element']?.details?.items?.[0] || null,
|
||||
clsShifts: lhr.audits['layout-shifts']?.details?.items || [],
|
||||
longTasks: lhr.audits['long-tasks']?.details?.items || []
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate Carbon Footprint
|
||||
try {
|
||||
const { checkGreenHosting, calculateCarbon } = require('./analysis/carbon-calculator');
|
||||
let transferSize = 0;
|
||||
const networkItems = lhr.audits['network-requests']?.details?.items || [];
|
||||
networkItems.forEach(item => {
|
||||
transferSize += item.transferSize || 0;
|
||||
});
|
||||
|
||||
const urlObj = new URL(lhr.finalUrl || url);
|
||||
const isGreen = await checkGreenHosting(urlObj.hostname);
|
||||
const carbonData = calculateCarbon(transferSize, isGreen);
|
||||
|
||||
summary.carbon = {
|
||||
co2: carbonData.co2,
|
||||
rating: carbonData.rating,
|
||||
isGreen: isGreen,
|
||||
transferSize: transferSize
|
||||
};
|
||||
} catch (carbonErr) {
|
||||
console.warn('Carbon calculation failed:', carbonErr);
|
||||
}
|
||||
|
||||
// Save JSON Summary
|
||||
let jsonPath = path.join(reportDir, `${testId}.json`);
|
||||
fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2));
|
||||
@@ -263,7 +291,7 @@ async function _executeTest(url, options) {
|
||||
options.userIp || null,
|
||||
isMobile,
|
||||
JSON.stringify(summary.scores),
|
||||
JSON.stringify(summary.metrics),
|
||||
JSON.stringify({ ...summary.metrics, carbon: summary.carbon, details: summary.details }),
|
||||
JSON.stringify(filmstripData)
|
||||
];
|
||||
|
||||
|
||||
15
main.js
15
main.js
@@ -124,6 +124,18 @@ function displayResults(data) {
|
||||
document.getElementById('performance-score').textContent = perfScore + '%';
|
||||
document.getElementById('structure-score').textContent = structureScore + '%';
|
||||
|
||||
// Carbon Footprint
|
||||
if (data.metrics.carbon) {
|
||||
document.getElementById('carbon-card').style.display = 'flex';
|
||||
document.getElementById('carbon-co2').textContent = data.metrics.carbon.co2 + 'g';
|
||||
document.getElementById('carbon-rating').textContent = data.metrics.carbon.rating;
|
||||
const isGreen = data.metrics.carbon.isGreen;
|
||||
const badge = document.getElementById('carbon-badge');
|
||||
badge.textContent = isGreen ? '🌱 Hosted Green' : '⛔ Non-Green Host';
|
||||
badge.style.background = isGreen ? 'rgba(16, 185, 129, 0.2)' : 'rgba(239, 68, 68, 0.2)';
|
||||
badge.style.color = isGreen ? 'var(--color-accent-success)' : 'var(--color-accent-danger)';
|
||||
}
|
||||
|
||||
// Web Vitals
|
||||
const lcpVal = data.metrics.lcp < 1000 ? (data.metrics.lcp/1000).toFixed(2) + 's' : Math.round(data.metrics.lcp) + 'ms';
|
||||
const tbtVal = Math.round(data.metrics.tbt) + 'ms';
|
||||
@@ -279,6 +291,9 @@ async function loadHistory() {
|
||||
<a href="/reports/${test.id}.html" target="_blank" class="btn-secondary" style="margin:0; padding: 0.25rem 0.75rem; font-size: 0.75rem;">
|
||||
View Report
|
||||
</a>
|
||||
<a href="/vitals.html?id=${test.id}" class="btn-secondary" style="margin:0; padding: 0.25rem 0.75rem; font-size: 0.75rem;" title="Deep Dive Analysis">
|
||||
⚡ Vitals
|
||||
</a>
|
||||
<div class="${colorClass}" style="font-weight: 700; font-size: 1.25rem">
|
||||
${perfScore}
|
||||
</div>
|
||||
|
||||
639
package-lock.json
generated
639
package-lock.json
generated
@@ -7,13 +7,15 @@
|
||||
"": {
|
||||
"name": "web-page-performance-test",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"chrome-launcher": "^1.2.1",
|
||||
"express": "^4.18.2",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"geoip-lite": "^1.4.10",
|
||||
"lighthouse": "^13.0.1",
|
||||
"pg": "^8.16.3",
|
||||
"puppeteer-core": "^21.0.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -641,33 +643,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.0.tgz",
|
||||
"integrity": "sha512-n6oQX6mYkG8TRPuPXmbPidkUbsSRalhmaaVAQxvH1IkQy63cwsH+kOjB3e4cpCDHg0aSvsiX9bQ4s2VB6mGWUQ==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz",
|
||||
"integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.3",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
"debug": "4.3.4",
|
||||
"extract-zip": "2.0.1",
|
||||
"progress": "2.0.3",
|
||||
"proxy-agent": "6.3.1",
|
||||
"tar-fs": "3.0.4",
|
||||
"unbzip2-stream": "1.4.3",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"browsers": "lib/cjs/main-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -679,9 +681,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
@@ -968,6 +970,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/atomically": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz",
|
||||
@@ -1012,6 +1019,7 @@
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
@@ -1098,6 +1106,26 @@
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz",
|
||||
@@ -1166,6 +1194,30 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
@@ -1213,6 +1265,43 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -1257,13 +1346,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-12.0.1.tgz",
|
||||
"integrity": "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg==",
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.8.tgz",
|
||||
"integrity": "sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
"mitt": "3.0.1",
|
||||
"urlpattern-polyfill": "10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
@@ -1311,7 +1400,6 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/configstore": {
|
||||
@@ -1368,6 +1456,15 @@
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/csp_evaluator": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.5.tgz",
|
||||
@@ -1781,6 +1878,20 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fluent-ffmpeg": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
|
||||
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^0.2.9",
|
||||
"which": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1805,6 +1916,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1829,6 +1946,33 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/geoip-lite": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/geoip-lite/-/geoip-lite-1.4.10.tgz",
|
||||
"integrity": "sha512-4N69uhpS3KFd97m00wiFEefwa+L+HT5xZbzPhwu+sDawStg6UN/dPwWtUfkQuZkGIY1Cj7wDVp80IsqNtGMi2w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "2.1 - 2.6.4",
|
||||
"chalk": "4.1 - 4.1.2",
|
||||
"iconv-lite": "0.4.13 - 0.6.3",
|
||||
"ip-address": "5.8.9 - 5.9.4",
|
||||
"lazy": "1.0.11",
|
||||
"rimraf": "2.5.2 - 2.7.1",
|
||||
"yauzl": "2.9.2 - 2.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/geoip-lite/node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
@@ -1927,6 +2071,27 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@@ -1940,6 +2105,28 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -2105,6 +2292,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
@@ -2130,6 +2337,17 @@
|
||||
"module-details-from-path": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -2149,12 +2367,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "5.9.4",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-5.9.4.tgz",
|
||||
"integrity": "sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"sprintf-js": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
@@ -2263,6 +2486,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
@@ -2278,6 +2507,21 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazy": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/lazy/-/lazy-1.0.11.tgz",
|
||||
"integrity": "sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/legacy-javascript": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz",
|
||||
@@ -2364,6 +2608,148 @@
|
||||
"integrity": "sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/@puppeteer/browsers": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.0.tgz",
|
||||
"integrity": "sha512-n6oQX6mYkG8TRPuPXmbPidkUbsSRalhmaaVAQxvH1IkQy63cwsH+kOjB3e4cpCDHg0aSvsiX9bQ4s2VB6mGWUQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.3",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"browsers": "lib/cjs/main-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/proxy-agent": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
||||
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"http-proxy-agent": "^7.0.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"lru-cache": "^7.14.1",
|
||||
"pac-proxy-agent": "^7.1.0",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"socks-proxy-agent": "^8.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/puppeteer-core": {
|
||||
"version": "24.34.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.34.0.tgz",
|
||||
"integrity": "sha512-24evawO+mUGW4mvS2a2ivwLdX3gk8zRLZr9HP+7+VT2vBQnm0oh9jJEZmUE3ePJhRkYlZ93i7OMpdcoi2qNCLg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.11.0",
|
||||
"chromium-bidi": "12.0.1",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1534754",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"webdriver-bidi-protocol": "0.3.10",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/puppeteer-core/node_modules/chromium-bidi": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-12.0.1.tgz",
|
||||
"integrity": "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/puppeteer-core/node_modules/devtools-protocol": {
|
||||
"version": "0.0.1534754",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz",
|
||||
"integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/puppeteer-core/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse/node_modules/tar-fs": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.22",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||
@@ -2481,6 +2867,12 @@
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/module-details-from-path": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
|
||||
@@ -2511,6 +2903,26 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
@@ -2713,6 +3125,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
@@ -2896,19 +3317,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
||||
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz",
|
||||
"integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"agent-base": "^7.0.2",
|
||||
"debug": "^4.3.4",
|
||||
"http-proxy-agent": "^7.0.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"lru-cache": "^7.14.1",
|
||||
"pac-proxy-agent": "^7.1.0",
|
||||
"pac-proxy-agent": "^7.0.1",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"socks-proxy-agent": "^8.0.5"
|
||||
"socks-proxy-agent": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -2961,30 +3382,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.34.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.34.0.tgz",
|
||||
"integrity": "sha512-24evawO+mUGW4mvS2a2ivwLdX3gk8zRLZr9HP+7+VT2vBQnm0oh9jJEZmUE3ePJhRkYlZ93i7OMpdcoi2qNCLg==",
|
||||
"version": "21.11.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-21.11.0.tgz",
|
||||
"integrity": "sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.11.0",
|
||||
"chromium-bidi": "12.0.1",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1534754",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"webdriver-bidi-protocol": "0.3.10",
|
||||
"ws": "^8.18.3"
|
||||
"@puppeteer/browsers": "1.9.1",
|
||||
"chromium-bidi": "0.5.8",
|
||||
"cross-fetch": "4.0.0",
|
||||
"debug": "4.3.4",
|
||||
"devtools-protocol": "0.0.1232444",
|
||||
"ws": "8.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=16.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -2996,21 +3416,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/devtools-protocol": {
|
||||
"version": "0.0.1534754",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz",
|
||||
"integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==",
|
||||
"version": "0.0.1232444",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz",
|
||||
"integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/puppeteer-core/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -3146,6 +3566,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/robots-parser": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz",
|
||||
@@ -3396,6 +3829,15 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socks/node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -3429,6 +3871,12 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -3516,17 +3964,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
|
||||
"integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
@@ -3555,6 +4000,12 @@
|
||||
"integrity": "sha512-h0JYX+dO2Zr3abCQpS6/uFjujaOjA1DyDzGQ41+oFn9VW/ARiq9g5ln7qEP9+BTzDpOMyIfsfj4OvfgXAsMUSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
|
||||
@@ -3602,6 +4053,12 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -3639,6 +4096,16 @@
|
||||
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unbzip2-stream": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
|
||||
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.2.1",
|
||||
"through": "^2.3.8"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
@@ -3661,6 +4128,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/urlpattern-polyfill": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
|
||||
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -3698,12 +4171,40 @@
|
||||
"integrity": "sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/when-exit": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
|
||||
"integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"dependencies": {
|
||||
"chrome-launcher": "^1.2.1",
|
||||
"express": "^4.18.2",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"geoip-lite": "^1.4.10",
|
||||
"lighthouse": "^13.0.1",
|
||||
"pg": "^8.16.3",
|
||||
"puppeteer-core": "^21.0.0",
|
||||
|
||||
72
server.js
72
server.js
@@ -3,6 +3,7 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { exec } = require("child_process");
|
||||
const runner = require("./lib/runner");
|
||||
const { traceAndLocate } = require("./lib/diagnostics/traceroute");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -99,17 +100,72 @@ app.get("*", (req, res) => {
|
||||
});
|
||||
|
||||
// API Endpoint: Traceroute
|
||||
app.get("/api/traceroute", (req, res) => {
|
||||
const { host } = req.query;
|
||||
// API Endpoint: Traceroute
|
||||
app.post("/api/traceroute", async (req, res) => {
|
||||
const { host } = req.body;
|
||||
if (!host) return res.status(400).json({ error: "Host required" });
|
||||
|
||||
// Sanitize host to prevent injection (basic)
|
||||
if (/[^a-zA-Z0-9.-]/.test(host)) return res.status(400).json({ error: "Invalid host" });
|
||||
try {
|
||||
const { raw, hops } = await traceAndLocate(host);
|
||||
res.json({ output: raw, hops });
|
||||
} catch (error) {
|
||||
console.error("Traceroute error:", error);
|
||||
res.status(500).json({ error: "Traceroute failed", details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
const cmd = process.platform === 'win32' ? `tracert -h 10 ${host}` : `traceroute -m 10 ${host}`;
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
res.json({ output: stdout || stderr });
|
||||
});
|
||||
// API Endpoint: Compare Videos
|
||||
app.get("/api/compare", async (req, res) => {
|
||||
const { test1, test2 } = req.query;
|
||||
if (!test1 || !test2) return res.status(400).send("Missing test IDs");
|
||||
|
||||
const reportDir = path.join(__dirname, 'reports');
|
||||
|
||||
// Helper to get frames
|
||||
const getFrames = (id) => {
|
||||
const p = path.join(reportDir, `${id}.json`);
|
||||
if (!fs.existsSync(p)) return null;
|
||||
const data = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
return data.filmstrip;
|
||||
};
|
||||
|
||||
try {
|
||||
const frames1 = getFrames(test1);
|
||||
const frames2 = getFrames(test2);
|
||||
|
||||
if (!frames1 || !frames2) return res.status(404).send("Test results not found or missing filmstrip");
|
||||
|
||||
// Dynamically import generator and stitcher
|
||||
const { createVideoFromFrames } = require('./lib/comparison/video-generator');
|
||||
const { stitchVideos } = require('./lib/comparison/stitcher');
|
||||
|
||||
// Generate individual videos
|
||||
// We assume 10 FPS as per capture logic
|
||||
const [video1, video2] = await Promise.all([
|
||||
createVideoFromFrames(frames1, 10),
|
||||
createVideoFromFrames(frames2, 10)
|
||||
]);
|
||||
|
||||
// Stitch them
|
||||
const outputFilename = `comparison-${test1}-${test2}.mp4`;
|
||||
const outputPath = path.join(reportDir, outputFilename);
|
||||
|
||||
await stitchVideos(video1, video2, outputPath);
|
||||
|
||||
// Cleanup temp individual videos
|
||||
try { fs.unlinkSync(video1); fs.unlinkSync(video2); } catch(e) {}
|
||||
|
||||
// Stream output
|
||||
res.setHeader('Content-Type', 'video/mp4');
|
||||
const stream = fs.createReadStream(outputPath);
|
||||
stream.pipe(res);
|
||||
|
||||
// Optional: Cleanup comparison file after streaming?
|
||||
// Or keep it cached. For now, keep it.
|
||||
} catch (err) {
|
||||
console.error("Comparison error:", err);
|
||||
res.status(500).send("Comparison failed");
|
||||
}
|
||||
});
|
||||
|
||||
// API Endpoint: Bulk Test
|
||||
|
||||
572
styles.css
572
styles.css
@@ -120,98 +120,151 @@ body::before {
|
||||
GTmetrix Dashboard
|
||||
========================================= */
|
||||
.gtmetrix-dashboard {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: var(--color-bg-tertiary); /* Darker internal card */
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 180px;
|
||||
background: var(--color-bg-tertiary); /* Darker internal card */
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.grade-card h3 { margin-bottom: 1rem; color: var(--color-text-secondary); font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.grade-card h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.grade-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3.5rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background: #444;
|
||||
transition: all 0.5s ease;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3.5rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background: #444;
|
||||
transition: all 0.5s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.grade-a { background: #0cce6b; box-shadow: 0 0 20px rgba(12, 206, 107, 0.4); }
|
||||
.grade-b { background: #0ba759; }
|
||||
.grade-c { background: #ffaa00; }
|
||||
.grade-d { background: #ff4e00; }
|
||||
.grade-f { background: #ff0000; }
|
||||
.grade-a {
|
||||
background: #0cce6b;
|
||||
box-shadow: 0 0 20px rgba(12, 206, 107, 0.4);
|
||||
}
|
||||
.grade-b {
|
||||
background: #0ba759;
|
||||
}
|
||||
.grade-c {
|
||||
background: #ffaa00;
|
||||
}
|
||||
.grade-d {
|
||||
background: #ff4e00;
|
||||
}
|
||||
.grade-f {
|
||||
background: #ff0000;
|
||||
}
|
||||
|
||||
.scores-card {
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.score-item { text-align: center; }
|
||||
.score-label { font-size: 0.9rem; color: var(--color-text-secondary); margin-bottom: 0.5rem; }
|
||||
.score-value { font-size: 2.5rem; font-weight: 700; color: var(--color-text-primary); }
|
||||
.score-divider { width: 1px; height: 60px; background: var(--color-border); }
|
||||
.score-item {
|
||||
text-align: center;
|
||||
}
|
||||
.score-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.score-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.score-divider {
|
||||
width: 1px;
|
||||
height: 60px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.vitals-card { align-items: stretch; }
|
||||
.vitals-card h3 { text-align: center; margin-bottom: 1rem; font-size: 0.9rem; color: var(--color-text-secondary); text-transform: uppercase; }
|
||||
.vitals-card {
|
||||
align-items: stretch;
|
||||
}
|
||||
.vitals-card h3 {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.vitals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.vital-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.vital-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.vital-label { font-size: 0.8rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.vital-value { font-size: 1.2rem; font-weight: 500; color: var(--color-accent); }
|
||||
|
||||
/* Filmstrip */
|
||||
.filmstrip-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 0;
|
||||
scrollbar-width: thin;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 0;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.filmstrip-frame {
|
||||
flex: 0 0 auto;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
flex: 0 0 auto;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
.filmstrip-frame img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 0.25rem;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.frame-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.frame-time { font-size: 0.75rem; color: var(--color-text-secondary); }
|
||||
|
||||
/* Checkbox */
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.checkbox-container input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.checkbox-container input { width: 16px; height: 16px; }
|
||||
|
||||
/* ===================================
|
||||
LAYOUT STRUCTURE
|
||||
@@ -323,94 +376,110 @@ body::before {
|
||||
|
||||
/* 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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* GitHub Link */
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-accent-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
transition: all var(--transition-base);
|
||||
backdrop-filter: blur(10px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(99, 102, 241, 0.1) 0%,
|
||||
rgba(139, 92, 246, 0.1) 100%
|
||||
);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-accent-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
transition: all var(--transition-base);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
border-color: rgba(99, 102, 241, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
color: var(--color-accent-primary);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(99, 102, 241, 0.2) 0%,
|
||||
rgba(139, 92, 246, 0.2) 100%
|
||||
);
|
||||
border-color: rgba(99, 102, 241, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.github-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Version Badge */
|
||||
.version-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-lg);
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
|
||||
border: 1.5px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 50px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
margin: 0 auto;
|
||||
transition: all var(--transition-base);
|
||||
backdrop-filter: blur(10px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-lg);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(16, 185, 129, 0.1) 0%,
|
||||
rgba(5, 150, 105, 0.1) 100%
|
||||
);
|
||||
border: 1.5px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 50px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
margin: 0 auto;
|
||||
transition: all var(--transition-base);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.version-badge:hover {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.15) 100%);
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(16, 185, 129, 0.15) 0%,
|
||||
rgba(5, 150, 105, 0.15) 100%
|
||||
);
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
transform: translateY(-1px) scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--color-accent-success);
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--color-accent-success);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.commit-id {
|
||||
color: var(--color-accent-success);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
|
||||
color: var(--color-accent-success);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.version-separator {
|
||||
color: rgba(16, 185, 129, 0.5);
|
||||
font-weight: 300;
|
||||
color: rgba(16, 185, 129, 0.5);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.commit-age {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 400;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
@@ -594,7 +663,9 @@ body::before {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Results Section */
|
||||
@@ -637,9 +708,18 @@ body::before {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.score-good { color: var(--color-accent-success); -webkit-text-fill-color: var(--color-accent-success); }
|
||||
.score-average { color: var(--color-accent-warning); -webkit-text-fill-color: var(--color-accent-warning); }
|
||||
.score-poor { color: var(--color-accent-danger); -webkit-text-fill-color: var(--color-accent-danger); }
|
||||
.score-good {
|
||||
color: var(--color-accent-success);
|
||||
-webkit-text-fill-color: var(--color-accent-success);
|
||||
}
|
||||
.score-average {
|
||||
color: var(--color-accent-warning);
|
||||
-webkit-text-fill-color: var(--color-accent-warning);
|
||||
}
|
||||
.score-poor {
|
||||
color: var(--color-accent-danger);
|
||||
-webkit-text-fill-color: var(--color-accent-danger);
|
||||
}
|
||||
|
||||
/* History List */
|
||||
.history-list {
|
||||
@@ -700,152 +780,190 @@ body::before {
|
||||
VIDEO PLAYER MODAL
|
||||
=================================== */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(5px);
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background: var(--color-bg-secondary);
|
||||
margin: 3% auto;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
width: 95%;
|
||||
max-width: 1400px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
position: relative;
|
||||
background: var(--color-bg-secondary);
|
||||
margin: 3% auto;
|
||||
padding: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
width: 95%;
|
||||
max-width: 1400px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--color-accent-danger);
|
||||
color: var(--color-accent-danger);
|
||||
}
|
||||
|
||||
.video-modal-content {
|
||||
max-width: 1200px;
|
||||
background: var(--color-bg-secondary);
|
||||
max-width: 1200px;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.video-player-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.video-frame {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-frame img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Carbon Card */
|
||||
.carbon-card {
|
||||
border-color: rgba(16, 185, 129, 0.2);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(16, 185, 129, 0.05) 0%,
|
||||
rgba(5, 150, 105, 0.05) 100%
|
||||
);
|
||||
}
|
||||
.carbon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.carbon-item {
|
||||
text-align: center;
|
||||
}
|
||||
.carbon-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.carbon-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.carbon-badge {
|
||||
margin-top: 1rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: var(--color-accent-success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
#video-play-btn {
|
||||
background: var(--color-accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
background: var(--color-accent-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#video-play-btn:hover {
|
||||
background: var(--color-accent-secondary);
|
||||
transform: translateY(-1px);
|
||||
background: var(--color-accent-secondary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.video-progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: var(--color-accent-primary);
|
||||
transition: width 0.1s linear;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: var(--color-accent-primary);
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
#video-time {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-primary);
|
||||
min-width: 4rem;
|
||||
text-align: right;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-primary);
|
||||
min-width: 4rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
212
traceroute.html
Normal file
212
traceroute.html
Normal file
@@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Network Traceroute - Web Page Performance Test</title>
|
||||
<link rel="icon" type="image/png" href="Logo.png" />
|
||||
<link rel="stylesheet" href="styles.css?v=2.2" />
|
||||
<!-- Leaflet CSS for Map -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
<style>
|
||||
#map {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.terminal-output {
|
||||
background: #1e1e1e;
|
||||
color: #0f0;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
margin-top: 1rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.hop-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.hop-item {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.hop-geo {
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header (Shared) -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<h1 class="title">
|
||||
<img src="Logo.png" alt="BCT Logo" class="title-icon" />
|
||||
Network Path Analysis
|
||||
</h1>
|
||||
<p class="subtitle">Traceroute & Geolocation</p>
|
||||
<!-- Navigation -->
|
||||
<nav style="margin-left: auto; display: flex; gap: 1rem">
|
||||
<a
|
||||
href="/"
|
||||
class="button"
|
||||
style="color: white; text-decoration: none"
|
||||
>⬅ Back to Speed Test</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-icon">🌍</div>
|
||||
<h2 class="panel-title">Trace Remote Host</h2>
|
||||
</div>
|
||||
|
||||
<div class="launcher-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="trace-host">Hostname or IP</label>
|
||||
<input
|
||||
type="text"
|
||||
id="trace-host"
|
||||
class="form-input"
|
||||
placeholder="google.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="trace-btn"
|
||||
class="btn-primary"
|
||||
onclick="runTraceroute()"
|
||||
>
|
||||
<span>Run Traceroute</span>
|
||||
<div
|
||||
id="loading-spinner"
|
||||
class="loading-spinner"
|
||||
style="display: none"
|
||||
></div>
|
||||
</button>
|
||||
<p
|
||||
id="error-msg"
|
||||
style="color: var(--color-accent-danger); display: none"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="results-area" class="results-container" style="display: none">
|
||||
<h3>Path Visualization</h3>
|
||||
<div id="map"></div>
|
||||
|
||||
<h3>Raw Output</h3>
|
||||
<div id="terminal-output" class="terminal-output"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2025 Beyond Cloud Technology.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
></script>
|
||||
|
||||
<script>
|
||||
let map = null;
|
||||
|
||||
async function runTraceroute() {
|
||||
const host = document.getElementById("trace-host").value;
|
||||
const btn = document.getElementById("trace-btn");
|
||||
const spinner = document.getElementById("loading-spinner");
|
||||
const errorMsg = document.getElementById("error-msg");
|
||||
const resultsArea = document.getElementById("results-area");
|
||||
const terminal = document.getElementById("terminal-output");
|
||||
|
||||
if (!host) return;
|
||||
|
||||
// UI Reset
|
||||
btn.disabled = true;
|
||||
spinner.style.display = "block";
|
||||
errorMsg.style.display = "none";
|
||||
resultsArea.style.display = "none";
|
||||
terminal.textContent =
|
||||
"Tracing... please wait (this can take up to 60s)...";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/traceroute", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ host }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) throw new Error(data.details || data.error);
|
||||
|
||||
// Show Results
|
||||
resultsArea.style.display = "block";
|
||||
terminal.textContent = data.output;
|
||||
|
||||
initMap(data.hops);
|
||||
} catch (err) {
|
||||
errorMsg.textContent = err.message;
|
||||
errorMsg.style.display = "block";
|
||||
terminal.textContent = "Error occurred.";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
spinner.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function initMap(hops) {
|
||||
if (map) {
|
||||
map.remove(); // Reset map
|
||||
}
|
||||
|
||||
// Initialize map (center on 0,0 default, or first hop)
|
||||
map = L.map("map").setView([20, 0], 2);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 18,
|
||||
attribution: "© OpenStreetMap",
|
||||
}).addTo(map);
|
||||
|
||||
const latlngs = [];
|
||||
|
||||
hops.forEach((hop) => {
|
||||
if (hop.lat && hop.lon) {
|
||||
const coord = [hop.lat, hop.lon];
|
||||
latlngs.push(coord);
|
||||
|
||||
L.marker(coord)
|
||||
.addTo(map)
|
||||
.bindPopup(
|
||||
`<b>${hop.ip}</b><br>${hop.city || ""}, ${hop.country || ""}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (latlngs.length > 0) {
|
||||
const polyline = L.polyline(latlngs, { color: "red" }).addTo(map);
|
||||
map.fitBounds(polyline.getBounds());
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
185
vitals.html
Normal file
185
vitals.html
Normal file
@@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Advanced Web Vitals - Web Page Performance Test</title>
|
||||
<link rel="icon" type="image/png" href="Logo.png">
|
||||
<link rel="stylesheet" href="styles.css?v=2.2">
|
||||
<style>
|
||||
.vitals-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
.metric-block {
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.metric-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.lcp-element-box {
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
overflow-x: auto;
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.cls-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.cls-table th, .cls-table td {
|
||||
text-align: left;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.cls-table th { color: var(--color-text-secondary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">Advanced Vitals Analysis</h1>
|
||||
<nav>
|
||||
<a href="/" class="btn-secondary">⬅ Dashboard</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="vitals-container" id="content-area">
|
||||
<div id="loading" style="text-align: center;">Loading Vitals Data...</div>
|
||||
|
||||
<!-- LCP Section -->
|
||||
<div id="lcp-section" class="metric-block" style="display:none;">
|
||||
<h2 class="metric-title">Largest Contentful Paint (LCP)</h2>
|
||||
<div style="display: flex; gap: 2rem; flex-wrap: wrap;">
|
||||
<div style="flex: 1; min-width: 300px;">
|
||||
<h3>LCP Element</h3>
|
||||
<div id="lcp-element-code" class="lcp-element-box"></div>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 300px;">
|
||||
<h3>Details</h3>
|
||||
<p><strong>Load Time:</strong> <span id="lcp-time" style="color: var(--color-accent); font-weight: bold;"></span></p>
|
||||
<p><strong>Phase Breakdown:</strong></p>
|
||||
<ul id="lcp-phases" style="list-style: none; padding-left: 0.5rem; line-height: 1.8;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CLS Section -->
|
||||
<div id="cls-section" class="metric-block" style="display:none;">
|
||||
<h2 class="metric-title">Cumulative Layout Shift (CLS)</h2>
|
||||
<h3>Shift Events</h3>
|
||||
<table class="cls-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Score</th>
|
||||
<th>Moved Elements</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cls-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Long Tasks Section -->
|
||||
<div id="tbt-section" class="metric-block" style="display:none;">
|
||||
<h2 class="metric-title">Total Blocking Time (TBT) & Long Tasks</h2>
|
||||
<div id="long-tasks-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const id = params.get('id');
|
||||
|
||||
if (!id) {
|
||||
document.getElementById('loading').textContent = 'No Test ID provided.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/reports/${id}.json`);
|
||||
if (!response.ok) throw new Error('Report not found');
|
||||
const data = await response.json();
|
||||
|
||||
renderVitals(data);
|
||||
} catch (e) {
|
||||
document.getElementById('loading').textContent = 'Error: ' + e.message;
|
||||
}
|
||||
});
|
||||
|
||||
function renderVitals(data) {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
const details = data.metrics.details || {}; // This comes from my runner.js update
|
||||
|
||||
// LCP
|
||||
const lcpSec = document.getElementById('lcp-section');
|
||||
if (details.lcpElement) {
|
||||
lcpSec.style.display = 'block';
|
||||
document.getElementById('lcp-element-code').textContent = details.lcpElement.node?.snippet || 'N/A';
|
||||
document.getElementById('lcp-time').textContent = data.metrics.lcp.toFixed(0) + 'ms';
|
||||
// phases if extracted, otherwise extract from audits in future
|
||||
}
|
||||
|
||||
// CLS
|
||||
const clsSec = document.getElementById('cls-section');
|
||||
if (details.clsShifts && details.clsShifts.length > 0) {
|
||||
clsSec.style.display = 'block';
|
||||
const tbody = document.getElementById('cls-tbody');
|
||||
details.clsShifts.forEach(shift => {
|
||||
const row = document.createElement('tr');
|
||||
// shift.node might be undefined if it's not element based or structured differently
|
||||
// Lighthouse CLS structure varies. usually items has `score`, `startTime`, `rects`?
|
||||
// Actually, items have `score` and `affectedWindows`.
|
||||
// Wait, `layout-shifts` or `cumulative-layout-shift` details structure:
|
||||
// It lists the shifts.
|
||||
// Let's assume standard properties or robust fallback.
|
||||
const time = shift.startTime ? (shift.startTime).toFixed(0) + 'ms' : '-';
|
||||
const score = shift.score ? shift.score.toFixed(4) : '0';
|
||||
const nodes = (shift.impactedNodes || []).map(n => n.node?.snippet).join('<br>') || 'Unknown';
|
||||
|
||||
row.innerHTML = `<td>${time}</td><td>${score}</td><td>${nodes}</td>`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// TBT / Long Tasks
|
||||
const tbtSec = document.getElementById('tbt-section');
|
||||
if (details.longTasks && details.longTasks.length > 0) {
|
||||
tbtSec.style.display = 'block';
|
||||
const list = document.getElementById('long-tasks-list');
|
||||
|
||||
// Sort by duration desc
|
||||
const tasks = details.longTasks.sort((a,b) => b.duration - a.duration).slice(0, 10);
|
||||
|
||||
tasks.forEach(task => {
|
||||
const div = document.createElement('div');
|
||||
div.style.marginBottom = '0.5rem';
|
||||
div.style.padding = '0.5rem';
|
||||
div.style.background = 'rgba(255,100,100,0.1)';
|
||||
div.style.borderLeft = '3px solid red';
|
||||
div.innerHTML = `<strong>${task.duration.toFixed(0)}ms</strong> at ${task.startTime.toFixed(0)}ms - ${task.url || 'Script Evaluation'}`;
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
if (!details.lcpElement && (!details.clsShifts || details.clsShifts.length === 0) && (!details.longTasks || details.longTasks.length === 0)) {
|
||||
document.getElementById('content-area').innerHTML += '<p>No advanced details available for this test. (Test might be old or failed to extract details)</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user