|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8" />
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
|
<title>Speed Test - Glassmorphism UI</title>
|
|
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap">
|
|
|
|
|
|
<style>
|
|
|
|
|
|
|
|
|
|
|
|
* {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
body {
|
|
|
font-family: 'Inter', sans-serif;
|
|
|
background: linear-gradient(to right, #0a0b0d, #17181b);
|
|
|
color: #fff;
|
|
|
min-height: 100vh;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: flex-start;
|
|
|
padding: 2rem;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.dashboard {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
|
gap: 1.5rem;
|
|
|
max-width: 1200px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.card {
|
|
|
position: relative;
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
border-radius: 1rem;
|
|
|
overflow: hidden;
|
|
|
backdrop-filter: blur(15px);
|
|
|
-webkit-backdrop-filter: blur(15px);
|
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
padding: 1.5rem;
|
|
|
}
|
|
|
.card::before {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
inset: 0;
|
|
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.02), rgba(255, 153, 0, 0.08));
|
|
|
pointer-events: none;
|
|
|
mix-blend-mode: overlay;
|
|
|
}
|
|
|
.card h2, .card h3, .card h4, .card h5, .card p {
|
|
|
position: relative;
|
|
|
margin-bottom: 0.4rem;
|
|
|
}
|
|
|
.card h2, .card h3 {
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.download-card .metric-value {
|
|
|
font-size: 2rem;
|
|
|
font-weight: 700;
|
|
|
margin-bottom: 0.2rem;
|
|
|
}
|
|
|
.download-card .sub-info {
|
|
|
font-size: 0.85rem;
|
|
|
opacity: 0.7;
|
|
|
margin-bottom: 0.6rem;
|
|
|
}
|
|
|
.download-card .download-icon {
|
|
|
font-size: 2rem;
|
|
|
color: #ff9900;
|
|
|
margin-bottom: 1rem;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.upload-card .metric-value {
|
|
|
font-size: 2rem;
|
|
|
font-weight: 700;
|
|
|
margin-bottom: 0.2rem;
|
|
|
}
|
|
|
.upload-card .sub-info {
|
|
|
font-size: 0.85rem;
|
|
|
opacity: 0.7;
|
|
|
margin-bottom: 0.6rem;
|
|
|
}
|
|
|
.upload-card .upload-icon {
|
|
|
font-size: 2rem;
|
|
|
color: #ff9900;
|
|
|
margin-bottom: 1rem;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.ping-card .metric-value {
|
|
|
font-size: 2rem;
|
|
|
font-weight: 700;
|
|
|
margin-bottom: 0.2rem;
|
|
|
}
|
|
|
.ping-card .sub-info {
|
|
|
font-size: 0.85rem;
|
|
|
opacity: 0.7;
|
|
|
margin-bottom: 0.6rem;
|
|
|
}
|
|
|
.ping-card .ping-icon {
|
|
|
font-size: 2rem;
|
|
|
color: #ff9900;
|
|
|
margin-bottom: 1rem;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.start-test-card {
|
|
|
background: url('https://via.placeholder.com/300x400?text=SpeedTest+Blur') center/cover no-repeat;
|
|
|
background-blend-mode: overlay;
|
|
|
background-color: rgba(0,0,0,0.5);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: flex-end;
|
|
|
color: #fff;
|
|
|
text-align: center;
|
|
|
padding: 2rem;
|
|
|
}
|
|
|
.start-test-card h3 {
|
|
|
font-size: 1.3rem;
|
|
|
margin-bottom: 0.4rem;
|
|
|
}
|
|
|
.start-test-card p {
|
|
|
font-size: 0.9rem;
|
|
|
opacity: 0.8;
|
|
|
margin-bottom: 1.2rem;
|
|
|
}
|
|
|
.start-btn {
|
|
|
display: inline-block;
|
|
|
background-color: rgba(255,255,255,0.1);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
padding: 0.8rem 1.2rem;
|
|
|
border-radius: 0.5rem;
|
|
|
color: #fff;
|
|
|
font-size: 1rem;
|
|
|
cursor: pointer;
|
|
|
transition: background 0.3s;
|
|
|
}
|
|
|
.start-btn:hover {
|
|
|
background-color: rgba(255, 153, 0, 0.2);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.clock-card {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
text-align: center;
|
|
|
}
|
|
|
.clock-card h4 {
|
|
|
margin-top: 1rem;
|
|
|
font-size: 1.2rem;
|
|
|
margin-bottom: 0.5rem;
|
|
|
}
|
|
|
.clock-face {
|
|
|
margin-top: 1rem;
|
|
|
width: 80px;
|
|
|
height: 80px;
|
|
|
border: 4px solid rgba(255,255,255,0.3);
|
|
|
border-radius: 50%;
|
|
|
position: relative;
|
|
|
}
|
|
|
.clock-face::before {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
left: 50%;
|
|
|
width: 4px;
|
|
|
height: 20px;
|
|
|
background: #ff9900;
|
|
|
transform: translate(-50%, -90%);
|
|
|
border-radius: 2px;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.progress-card .progress-icon {
|
|
|
font-size: 2rem;
|
|
|
color: #ff9900;
|
|
|
margin-bottom: 1rem;
|
|
|
}
|
|
|
.progress-card .progress-value {
|
|
|
font-size: 1.6rem;
|
|
|
font-weight: 600;
|
|
|
margin-bottom: 0.2rem;
|
|
|
}
|
|
|
.progress-card .small-text {
|
|
|
font-size: 0.85rem;
|
|
|
opacity: 0.7;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.results-card {
|
|
|
text-align: center;
|
|
|
}
|
|
|
.results-card h4 {
|
|
|
font-size: 1.2rem;
|
|
|
margin-bottom: 0.8rem;
|
|
|
}
|
|
|
.results-card .summary {
|
|
|
font-size: 0.9rem;
|
|
|
opacity: 0.8;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
|
|
|
<div class="dashboard">
|
|
|
|
|
|
<div class="card download-card">
|
|
|
<i class="fas fa-download download-icon"></i>
|
|
|
<h3>Download Speed</h3>
|
|
|
<div class="metric-value" id="downloadValue">--</div>
|
|
|
<p class="sub-info">Mbps</p>
|
|
|
<p class="sub-info">(<span id="downloadMBps">--</span> MB/s)</p>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="card upload-card">
|
|
|
<i class="fas fa-upload upload-icon"></i>
|
|
|
<h3>Upload Speed</h3>
|
|
|
<div class="metric-value" id="uploadValue">--</div>
|
|
|
<p class="sub-info">Mbps</p>
|
|
|
<p class="sub-info">(<span id="uploadMBps">--</span> MB/s)</p>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="card ping-card">
|
|
|
<i class="fas fa-stopwatch ping-icon"></i>
|
|
|
<h3>Ping</h3>
|
|
|
<div class="metric-value" id="pingValue">--</div>
|
|
|
<p class="sub-info">ms</p>
|
|
|
<p class="sub-info">Rating: <span id="pingRating">--</span></p>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="card start-test-card">
|
|
|
<h3>Speed Test Ready</h3>
|
|
|
<p>Tap below to begin measuring your connection.</p>
|
|
|
<button class="start-btn" id="startTest">Start Test</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="card clock-card">
|
|
|
<h4>Test Status</h4>
|
|
|
<div class="clock-face"></div>
|
|
|
<p id="statusText" style="margin-top:1rem; opacity:0.8;">Not started</p>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="card progress-card">
|
|
|
<i class="fas fa-spinner progress-icon"></i>
|
|
|
<h4>Progress</h4>
|
|
|
<div class="progress-value" id="progressPercent">0%</div>
|
|
|
<p class="small-text">Current Step</p>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="card results-card">
|
|
|
<h4>Test Summary</h4>
|
|
|
<p class="summary" id="resultSummary">No data yet.</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
const startBtn = document.getElementById('startTest');
|
|
|
const statusText = document.getElementById('statusText');
|
|
|
const progressPercent = document.getElementById('progressPercent');
|
|
|
const downloadValue = document.getElementById('downloadValue');
|
|
|
const uploadValue = document.getElementById('uploadValue');
|
|
|
const pingValue = document.getElementById('pingValue');
|
|
|
const downloadMBps = document.getElementById('downloadMBps');
|
|
|
const pingRating = document.getElementById('pingRating');
|
|
|
const resultSummary = document.getElementById('resultSummary');
|
|
|
|
|
|
let testId = null;
|
|
|
let pollInterval = null;
|
|
|
|
|
|
startBtn.addEventListener('click', async () => {
|
|
|
|
|
|
statusText.textContent = "Starting test...";
|
|
|
progressPercent.textContent = "0%";
|
|
|
downloadValue.textContent = "--";
|
|
|
uploadValue.textContent = "--";
|
|
|
pingValue.textContent = "--";
|
|
|
pingRating.textContent = "--";
|
|
|
downloadMBps.textContent = "--";
|
|
|
resultSummary.textContent = "Measuring your connection...";
|
|
|
|
|
|
|
|
|
try {
|
|
|
const res = await fetch('/start-test', { method: 'POST' });
|
|
|
const data = await res.json();
|
|
|
testId = data.test_id;
|
|
|
|
|
|
pollInterval = setInterval(fetchTestStatus, 1000);
|
|
|
} catch (error) {
|
|
|
statusText.textContent = "Error starting test.";
|
|
|
console.error("Error:", error);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
async function fetchTestStatus() {
|
|
|
if (!testId) return;
|
|
|
try {
|
|
|
const res = await fetch(`/test-status/${testId}`);
|
|
|
if (res.status !== 200) {
|
|
|
statusText.textContent = "Test not found.";
|
|
|
clearInterval(pollInterval);
|
|
|
return;
|
|
|
}
|
|
|
const result = await res.json();
|
|
|
|
|
|
|
|
|
statusText.textContent = result.status;
|
|
|
progressPercent.textContent = result.progress + "%";
|
|
|
if (result.download_mbps) {
|
|
|
downloadValue.textContent = result.download_mbps;
|
|
|
downloadMBps.textContent = result.download_MBps;
|
|
|
}
|
|
|
if (result.upload_mbps) {
|
|
|
uploadValue.textContent = result.upload_mbps;
|
|
|
document.getElementById('uploadMBps').textContent = result.upload_MBps;
|
|
|
}
|
|
|
if (result.ping) {
|
|
|
pingValue.textContent = result.ping;
|
|
|
pingRating.textContent = result.ping <= 20 ? "Excellent" :
|
|
|
result.ping <= 50 ? "Good" :
|
|
|
result.ping <= 100 ? "Average" : "Poor";
|
|
|
}
|
|
|
if (result.progress === 100 || result.progress < 0) {
|
|
|
|
|
|
clearInterval(pollInterval);
|
|
|
resultSummary.textContent = `Download: ${result.download_mbps} Mbps, Upload: ${result.upload_mbps} Mbps, Ping: ${result.ping} ms`;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error("Error fetching test status:", error);
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|
|
|
|