Implement the new design from #649 (#694)

* Implement fromScratch design

* Add some documentation

* enable new design in docker (no alpine yet)

* merge fixed docker images

* alpine docker added new design

* fix #685

* Implement fromScratch design

* Add some documentation

* enable new design in docker (no alpine yet)

* alpine docker added new design

* fix #685

* fix database permissions alpine, remove baby

* hide serverselector on only one server

* Update frontend/styling/server-selector.css

Co-authored-by: qodo-merge-pro-for-open-source[bot] <189517486+qodo-merge-pro-for-open-source[bot]@users.noreply.github.com>

* fix alpine image again

* adjust settings.json in entrypoint

so env vars are honored

* Update frontend/javascript/index.js

Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com>

* Add feature switch for new design via config file, URL parameters, and Docker (#742)

* Initial plan

* Add feature switch for new design with config and URL parameter support

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Improve error handling and prevent infinite redirect loops

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Update Dockerfiles and entrypoint to support design feature switch

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Update design-switch.js

Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com>

* fix: copy actions in entrypoint

* Restructure design switch to place both designs at root level

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Flatten frontend assets in Docker to eliminate frontend directory

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* fix: entrypoint settings & server-list

disable entrypoint bash debug

* add link to modern design

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>
Co-authored-by: sstidl <sstidl@users.noreply.github.com>
Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com>

* add armv7

* reformat

* Add GDPR_EMAIL environment variable for Docker deployments (#743)

* Initial plan

* Add GDPR_EMAIL environment variable for Docker deployments

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Improve GDPR_EMAIL handling with proper escaping and loop

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Refine GDPR_EMAIL processing - skip index.html and improve escaping

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Optimize GDPR_EMAIL sed commands and improve escaping

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* Clarify sed escaping comment for GDPR_EMAIL

Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>

* cleanup old EMAIL ENV Var

* fix: line break in html prevented sed replacement

* version 6.0.0pre1

* test: add mssql docker compose tests

* Update Speedtest screen recording link in README

* Filter unreachable servers from selector (newdesign UI) (#769)

* Filter unreachable servers from selector (newdesign UI)

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* keep // servers in list

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix misleading comment

---------

Co-authored-by: Lumi <lumi@openclaw.local>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stefan Stidl <sti-github@stidl.com>

* fix: server.json handling

* Use server-list.json in classic frontend by default

* Add configurable server list URLs to frontend and Docker

---------

Co-authored-by: Timendus <mail@timendus.com>
Co-authored-by: Stefan Stidl <stefan.stidl@ffg.at>
Co-authored-by: qodo-merge-pro-for-open-source[bot] <189517486+qodo-merge-pro-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sstidl <12804296+sstidl@users.noreply.github.com>
Co-authored-by: qodo-free-for-open-source-projects[bot] <189517486+qodo-free-for-open-source-projects[bot]@users.noreply.github.com>
Co-authored-by: Stefan Stidl <sti-github@stidl.com>
Co-authored-by: Lumi <lumi@openclaw.local>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
sstidl 2026-04-11 23:07:05 +02:00 committed by GitHub
parent e1310d88a3
commit 3a7dc31ff2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 3178 additions and 1087 deletions

View file

@ -1,7 +1,17 @@
#!/bin/bash
echo "Setting up docker env..."
echo "MODE: $MODE"
echo "USE_NEW_DESIGN: $USE_NEW_DESIGN"
echo "SERVER_LIST_URL: $SERVER_LIST_URL"
echo "WEBPORT: $WEBPORT"
echo "REDACT_IP_ADDRESSES: $REDACT_IP_ADDRESSES"
echo "DB_TYPE: $DB_TYPE"
echo "ENABLE_ID_OBFUSCATION: $ENABLE_ID_OBFUSCATION"
echo "GDPR_EMAIL: $GDPR_EMAIL"
set -e
set -x
#set -x
is_alpine() {
[ -f /etc/alpine-release ]
@ -13,6 +23,10 @@ rm -rf /var/www/html/*
# Copy frontend files
cp /speedtest/*.js /var/www/html/
# Copy design switch files
cp /speedtest/config.json /var/www/html/
cp /speedtest/design-switch.js /var/www/html/
# Copy favicon
cp /speedtest/favicon.ico /var/www/html/
@ -40,14 +54,58 @@ if [ "$MODE" == "backend" ]; then
fi
fi
# Set up unified index.php
if [ "$MODE" != "backend" ]; then
cp /speedtest/ui.php /var/www/html/index.php
# Set up index.php for frontend-only or standalone modes
if [[ "$MODE" == "frontend" || "$MODE" == "dual" || "$MODE" == "standalone" ]]; then
# Copy design files (switcher + both designs)
cp /speedtest/index.html /var/www/html/
cp /speedtest/index-classic.html /var/www/html/
cp /speedtest/index-modern.html /var/www/html/
# Copy frontend assets directly to root-level subdirectories (no frontend/ parent dir)
mkdir -p /var/www/html/styling /var/www/html/javascript /var/www/html/images /var/www/html/fonts
cp -a /speedtest/frontend/styling/* /var/www/html/styling/
cp -a /speedtest/frontend/javascript/* /var/www/html/javascript/
cp -a /speedtest/frontend/images/* /var/www/html/images/
cp -a /speedtest/frontend/fonts/* /var/www/html/fonts/ 2>/dev/null || true
# Copy frontend config files
cp /speedtest/frontend/settings.json /var/www/html/settings.json 2>/dev/null || true
if [ -f /servers.json ]; then
echo "using mounted /servers.json for server-list.json"
cp /servers.json /var/www/html/server-list.json
else
echo "no /servers.json found, create one for local host"
# generate config for just the local server
echo '[{"name":"local","server":"/backend", "dlURL": "garbage.php", "ulURL": "empty.php", "pingURL": "empty.php", "getIpURL": "getIP.php", "sponsorName": "", "sponsorURL": "", "id":1 }]' > /var/www/html/server-list.json
fi
if [ ! -z "$SERVER_LIST_URL" ]; then
echo "using SERVER_LIST_URL for frontend server list"
SERVER_LIST_URL_ESCAPED=$(printf '%s\n' "$SERVER_LIST_URL" | sed 's/[&/\\]/\\&/g; s/\$/\\$/g')
sed -i "s/var SPEEDTEST_SERVERS = \"server-list.json\";/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";/" /var/www/html/index-modern.html
sed -i "s/var SPEEDTEST_SERVERS = \\[/var SPEEDTEST_SERVERS = \"$SERVER_LIST_URL_ESCAPED\";\\n\\t\\t\\/\\*/" /var/www/html/index-classic.html
fi
# Replace GDPR email placeholder if GDPR_EMAIL is set
if [ ! -z "$GDPR_EMAIL" ]; then
# Escape special sed characters: & (replacement), / (delimiter), \ (escape), $ (variable)
GDPR_EMAIL_ESCAPED=$(printf '%s\n' "$GDPR_EMAIL" | sed 's/[&/\\]/\\&/g; s/\$/\\$/g')
for html_file in /var/www/html/index-modern.html /var/www/html/index-classic.html; do
if [ -f "$html_file" ]; then
sed -i "s/TO BE FILLED BY DEVELOPER/$GDPR_EMAIL_ESCAPED/g; s/PUT@YOUR_EMAIL.HERE/$GDPR_EMAIL_ESCAPED/g" "$html_file"
fi
done
fi
fi
# Configure design preference via config.json
if [ "$USE_NEW_DESIGN" == "true" ]; then
sed -i 's/"useNewDesign": false/"useNewDesign": true/' /var/www/html/config.json
fi
# Apply Telemetry settings when running in standalone or frontend mode and telemetry is enabled
if [[ "$TELEMETRY" == "true" && ("$MODE" == "frontend" || "$MODE" == "standalone" || "$MODE" == "dual") ]]; then
cp -r /speedtest/results /var/www/html/results
sed -i 's/telemetry_level": ".*"/telemetry_level": "basic"/' /var/www/html/settings.json
if [ "$MODE" == "frontend" ]; then
mkdir /var/www/html/backend

View file

@ -4,14 +4,14 @@ x-shared:
speedtest-service: &speedtest-service
environment:
- PASSWORD=gimmeTheStats1337
- EMAIL=test@example.com
- GDPR_EMAIL=test@example.com
- ENABLE_ID_OBFUSCATION=true
- MODE=standalone
- REDACT_IP_ADDRESSES=true
- TELEMETRY=true
services:
###################### POSTGRESQL ################################
###################### POSTGRESQL ################################
pg:
image: postgres:alpine
environment:
@ -32,7 +32,7 @@ services:
- DB_PASSWORD=Not4SecureProduction
- DB_TYPE=postgresql
- DB_USERNAME=librespeed
- EMAIL=test@example.com
- GDPR_EMAIL=test@example.com
- ENABLE_ID_OBFUSCATION=true
- MODE=standalone
- REDACT_IP_ADDRESSES=true
@ -48,7 +48,7 @@ services:
ports:
- 9124:8080
####################### MYSQL ##############################
####################### MYSQL ##############################
mysql:
image: mysql:lts
@ -73,7 +73,7 @@ services:
- DB_PASSWORD=Not4SecureProduction
- DB_TYPE=mysql
- DB_USERNAME=librespeed
- EMAIL=test@example.com
- GDPR_EMAIL=test@example.com
- ENABLE_ID_OBFUSCATION=true
- MODE=standalone
- REDACT_IP_ADDRESSES=true
@ -88,7 +88,72 @@ services:
ports:
- 9126:8080
################ SQLITE ####################################
####################### MSSQL ##############################
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=Not4SecureProduction1337
- MSSQL_PID=Express
healthcheck:
test:
["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $$SA_PASSWORD -C -Q 'SELECT 1' || exit 1"]
interval: 10s
retries: 5
start_period: 30s
mssql-init:
image: mcr.microsoft.com/mssql/server:2022-latest
depends_on:
mssql:
condition: service_healthy
environment:
- SA_PASSWORD=Not4SecureProduction1337
volumes:
- ../../results/telemetry_mssql.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
command: >
bash -c '
echo "Waiting for MSSQL to be ready...";
until /opt/mssql-tools18/bin/sqlcmd -S mssql -U sa -P "$$SA_PASSWORD" -C -Q "SELECT 1"; do
sleep 1;
done;
echo "Running init scripts...";
for f in /docker-entrypoint-initdb.d/*.sql; do
echo "Executing $$f...";
/opt/mssql-tools18/bin/sqlcmd -S mssql -U sa -P "$$SA_PASSWORD" -C -i "$$f";
done;
echo "Initialization complete!";
'
speedtest-debian-mssql:
build:
context: ../..
dockerfile: Dockerfile
environment: &env_vars_mssql
- PASSWORD=gimmeTheStats1337
- DB_HOSTNAME=mssql
- DB_NAME=librespeed
- DB_PASSWORD=Not4SecureProduction1337
- DB_TYPE=mssql
- DB_USERNAME=sa
- GDPR_EMAIL=test@example.com
- ENABLE_ID_OBFUSCATION=true
- MODE=standalone
- REDACT_IP_ADDRESSES=true
- TELEMETRY=true
ports:
- 9130:8080
speedtest-alpine-mssql:
build:
context: ../..
dockerfile: Dockerfile.alpine
environment: *env_vars_mssql
ports:
- 9131:8080
################ SQLITE ####################################
speedtest-debian-sqlite:
# check at http://localhost:9125/results/sanitycheck.php
@ -97,7 +162,7 @@ services:
dockerfile: Dockerfile
environment: &env_vars_sqlite
- PASSWORD=gimmeTheStats1337
- EMAIL=test@example.com
- GDPR_EMAIL=test@example.com
- ENABLE_ID_OBFUSCATION=true
- MODE=standalone
- REDACT_IP_ADDRESSES=true
@ -126,14 +191,16 @@ services:
ports:
- 9129:8080
############## TEST CONTAINER ###############################################################
############## TEST CONTAINER ###############################################################
test-container:
image: alpine
depends_on:
- pg
- mysql
- mssql-init
- speedtest-alpine-mysql
- speedtest-alpine-pg
- speedtest-alpine-mssql
volumes:
- ./test-script.sh:/test-script.sh
command:

View file

@ -12,7 +12,7 @@ fi
echo sleeping a little to get things settled...
sleep 15
for db in sqlite pg mysql; do
for db in sqlite pg mysql mssql; do
for distro in alpine debian; do
hostname=speedtest-$distro-$db
echo $hostname

View file

@ -1,528 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<meta charset="UTF-8" />
<script type="text/javascript" src="speedtest.js"></script>
<script type="text/javascript">
function I(i){return document.getElementById(i);}
//LIST OF TEST SERVERS. See documentation for details if needed
<?php
$mode=getenv("MODE");
if($mode=="standalone" || $mode=="backend"){ ?>
var SPEEDTEST_SERVERS=[];
<?php } else { ?>
var SPEEDTEST_SERVERS= <?= file_get_contents('/servers.json') ?: '[]' ?>;
<?php }
if ($mode=="dual"){ ?>
// add own server in dual mode
SPEEDTEST_SERVERS.unshift({
"name":"This Server",
"server":document.location.href+"backend/",
"id":1,
"dlURL":"garbage.php",
"ulURL":"empty.php",
"pingURL":"empty.php",
"getIpURL":"getIP.php",
"pingT":-1
})
<?php } ?>
//INITIALIZE SPEED TEST
var s=new Speedtest(); //create speed test object
<?php if(getenv("TELEMETRY")=="true"){ ?>
s.setParameter("telemetry_level","basic");
<?php } ?>
<?php if(getenv("DISABLE_IPINFO")=="true"){ ?>
s.setParameter("getIp_ispInfo",false);
<?php } ?>
<?php if(getenv("DISTANCE")){ ?>
s.setParameter("getIp_ispInfo_distance","<?=getenv("DISTANCE") ?>");
<?php } ?>
//SERVER AUTO SELECTION
function initServers(){
if(SPEEDTEST_SERVERS.length==0){ //standalone installation
//just make the UI visible
I("loading").className="hidden";
I("serverArea").style.display="none";
I("testWrapper").className="visible";
initUI();
}else{ //multiple servers
var noServersAvailable=function(){
I("message").innerHTML="No servers available";
}
var runServerSelect=function(){
s.selectServer(function(server){
if(server!=null){ //at least 1 server is available
I("loading").className="hidden"; //hide loading message
//populate server list for manual selection
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
var option=document.createElement("option");
option.value=i;
option.textContent=SPEEDTEST_SERVERS[i].name;
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
I("server").appendChild(option);
}
//show test UI
I("testWrapper").className="visible";
initUI();
}else{ //no servers are available, the test cannot proceed
noServersAvailable();
}
});
}
if(typeof SPEEDTEST_SERVERS === "string"){
//need to fetch list of servers from specified URL
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
if(servers==null){ //failed to load server list
noServersAvailable();
}else{ //server list loaded
SPEEDTEST_SERVERS=servers;
runServerSelect();
}
});
}else{
//hardcoded server list
s.addTestPoints(SPEEDTEST_SERVERS);
runServerSelect();
}
}
}
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
var dlColor="#6060AA",
ulColor="#616161";
var progColor=meterBk;
//CODE FOR GAUGES
function drawMeter(c,amount,bk,fg,progress,prog){
var ctx=c.getContext("2d");
var dp=window.devicePixelRatio||1;
var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
var sizScale=ch*0.0055;
if(c.width==cw&&c.height==ch){
ctx.clearRect(0,0,cw,ch);
}else{
c.width=cw;
c.height=ch;
}
ctx.beginPath();
ctx.strokeStyle=bk;
ctx.lineWidth=12*sizScale;
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle=fg;
ctx.lineWidth=12*sizScale;
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
ctx.stroke();
if(typeof progress !== "undefined"){
ctx.fillStyle=prog;
ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
}
}
function mbpsToAmount(s){
return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
}
function format(d){
d=Number(d);
if(d<10) return d.toFixed(2);
if(d<100) return d.toFixed(1);
return d.toFixed(0);
}
//UI CODE
var uiData=null;
function startStop(){
if(s.getState()==3){
//speed test is running, abort
s.abort();
data=null;
I("startStopBtn").className="";
I("server").disabled=false;
initUI();
}else{
//test is not running, begin
I("startStopBtn").className="running";
I("shareArea").style.display="none";
I("server").disabled=true;
s.onupdate=function(data){
uiData=data;
};
s.onend=function(aborted){
I("startStopBtn").className="";
I("server").disabled=false;
updateUI(true);
if(!aborted){
//if testId is present, show sharing panel, otherwise do nothing
try{
var testId=uiData.testId;
if(testId!=null){
var shareURL=window.location.href.substring(0,window.location.href.lastIndexOf("/"))+"/results/?id="+testId;
I("resultsImg").src=shareURL;
I("resultsImg").alt="Download: "+uiData.dlStatus+" Mbps, Upload: "+uiData.ulStatus+" Mbps, Ping: "+uiData.pingStatus+" ms, Jitter: "+uiData.jitterStatus+" ms";
I("resultsURL").value=shareURL;
I("testId").innerHTML=testId;
I("shareArea").style.display="";
}
}catch(e){}
}
};
s.start();
}
}
//this function reads the data sent back by the test and updates the UI
function updateUI(forced){
if(!forced&&s.getState()!=3) return;
if(uiData==null) return;
var status=uiData.testState;
I("ip").textContent=uiData.clientIp;
I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"...":format(uiData.dlStatus);
drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"...":format(uiData.ulStatus);
drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
I("pingText").textContent=format(uiData.pingStatus);
I("jitText").textContent=format(uiData.jitterStatus);
}
function oscillate(){
return 1+0.02*Math.sin(Date.now()/100);
}
//update the UI every frame
window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
function frame(){
requestAnimationFrame(frame);
updateUI();
}
frame(); //start frame loop
//function to (re)initialize UI
function initUI(){
drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
I("dlText").textContent="";
I("ulText").textContent="";
I("pingText").textContent="";
I("jitText").textContent="";
I("ip").textContent="";
}
</script>
<style type="text/css">
html,body{
border:none; padding:0; margin:0;
background:#FFFFFF;
color:#202020;
}
body{
text-align:center;
font-family:"Roboto",sans-serif;
}
h1{
color:#404040;
}
#loading{
background-color:#FFFFFF;
color:#404040;
text-align:center;
}
span.loadCircle{
display:inline-block;
width:2em;
height:2em;
vertical-align:middle;
background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAP1BMVEUAAAB2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZyFzwnAAAAFHRSTlMAEvRFvX406baecwbf0casimhSHyiwmqgAAADpSURBVHja7dbJbQMxAENRahnN5lkc//5rDRAkDeRgHszXgACJoKiIiIiIiIiIiIiIiIiIiIj4HHspsrpAVhdVVguzrA4OWc10WcEqpwKbnBo0OU1Q5NSpsoJFTgOecrrdEag85DRgktNqfoEdTjnd7hrEHMEJvmRUYJbTYk5Agy6nau6Abp5Cm7mDBtRdPi9gyKdU7w4p1fsLvyqs8hl4z9/w3n/Hmr9WoQ65lAU4d7lMYOz//QboRR5jBZibLMZdAR6O/Vfa1PlxNr3XdS3HzK/HVPRu/KnLs8iAOh993VpRRERERMT/fAN60wwWaVyWwAAAAABJRU5ErkJggg==');
background-size:2em 2em;
margin-right:0.5em;
animation: spin 0.6s linear infinite;
}
@keyframes spin{
0%{transform:rotate(0deg);}
100%{transform:rotate(359deg);}
}
#startStopBtn{
display:inline-block;
margin:0 auto;
color:#6060AA;
background-color:rgba(0,0,0,0);
border:0.15em solid #6060FF;
border-radius:0.3em;
transition:all 0.3s;
box-sizing:border-box;
width:8em; height:3em;
line-height:2.7em;
cursor:pointer;
box-shadow: 0 0 0 rgba(0,0,0,0.1), inset 0 0 0 rgba(0,0,0,0.1);
}
#startStopBtn:hover{
box-shadow: 0 0 2em rgba(0,0,0,0.1), inset 0 0 1em rgba(0,0,0,0.1);
}
#startStopBtn.running{
background-color:#FF3030;
border-color:#FF6060;
color:#FFFFFF;
}
#startStopBtn:before{
content:"Start";
}
#startStopBtn.running:before{
content:"Abort";
}
#serverArea{
margin-top:1em;
}
#server{
font-size:1em;
padding:0.2em;
}
#test{
margin-top:2em;
margin-bottom:12em;
}
div.testArea{
display:inline-block;
width:16em;
height:12.5em;
position:relative;
box-sizing:border-box;
}
div.testArea2{
display:inline-block;
width:14em;
height:7em;
position:relative;
box-sizing:border-box;
text-align:center;
}
div.testArea div.testName{
position:absolute;
top:0.1em; left:0;
width:100%;
font-size:1.4em;
z-index:9;
}
div.testArea2 div.testName{
display:block;
text-align:center;
font-size:1.4em;
}
div.testArea div.meterText{
position:absolute;
bottom:1.55em; left:0;
width:100%;
font-size:2.5em;
z-index:9;
}
div.testArea2 div.meterText{
display:inline-block;
font-size:2.5em;
}
div.meterText:empty:before{
content:"0.00";
}
div.testArea div.unit{
position:absolute;
bottom:2em; left:0;
width:100%;
z-index:9;
}
div.testArea2 div.unit{
display:inline-block;
}
div.testArea canvas{
position:absolute;
top:0; left:0; width:100%; height:100%;
z-index:1;
}
div.testGroup{
display:block;
margin: 0 auto;
}
#shareArea{
width:95%;
max-width:40em;
margin:0 auto;
margin-top:2em;
}
#shareArea > *{
display:block;
width:100%;
height:auto;
margin: 0.25em 0;
}
#privacyPolicy{
position:fixed;
top:2em;
bottom:2em;
left:2em;
right:2em;
overflow-y:auto;
width:auto;
height:auto;
box-shadow:0 0 3em 1em #000000;
z-index:999999;
text-align:left;
background-color:#FFFFFF;
padding:1em;
}
a.privacy{
text-align:center;
font-size:0.8em;
color:#808080;
padding: 0 3em;
}
div.closePrivacyPolicy {
width: 100%;
text-align: center;
}
div.closePrivacyPolicy a.privacy {
padding: 1em 3em;
}
@media all and (max-width:40em){
body{
font-size:0.8em;
}
}
div.visible{
animation: fadeIn 0.4s;
display:block;
}
div.hidden{
animation: fadeOut 0.4s;
display:none;
}
@keyframes fadeIn{
0%{
opacity:0;
}
100%{
opacity:1;
}
}
@keyframes fadeOut{
0%{
display:block;
opacity:1;
}
100%{
display:block;
opacity:0;
}
}
@media all and (prefers-color-scheme: dark){
html,body,#loading{
background:#202020;
color:#F4F4F4;
color-scheme:dark;
}
h1{
color:#E0E0E0;
}
a{
color:#9090FF;
}
#privacyPolicy{
background:#000000;
}
#resultsImg{
filter: invert(1);
}
}
</style>
<title><?= getenv('TITLE') ?: 'LibreSpeed' ?></title>
</head>
<body onload="initServers()">
<h1><?= getenv('TITLE') ?: 'LibreSpeed' ?></h1>
<div id="loading" class="visible">
<p id="message"><span class="loadCircle"></span>Selecting a server...</p>
</div>
<div id="testWrapper" class="hidden">
<div id="startStopBtn" onclick="startStop()"></div><br/>
<?php if(getenv("TELEMETRY")=="true"){ ?>
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display=''">Privacy</a>
<?php } ?>
<div id="serverArea">
Server: <select id="server" onchange="s.setSelectedServer(SPEEDTEST_SERVERS[this.value])"></select>
</div>
<div id="test">
<div class="testGroup">
<div class="testArea2">
<div class="testName">Ping</div>
<div id="pingText" class="meterText" style="color:#AA6060"></div>
<div class="unit">ms</div>
</div>
<div class="testArea2">
<div class="testName">Jitter</div>
<div id="jitText" class="meterText" style="color:#AA6060"></div>
<div class="unit">ms</div>
</div>
</div>
<div class="testGroup">
<div class="testArea">
<div class="testName">Download</div>
<canvas id="dlMeter" class="meter"></canvas>
<div id="dlText" class="meterText"></div>
<div class="unit">Mbit/s</div>
</div>
<div class="testArea">
<div class="testName">Upload</div>
<canvas id="ulMeter" class="meter"></canvas>
<div id="ulText" class="meterText"></div>
<div class="unit">Mbit/s</div>
</div>
</div>
<div id="ipArea">
<span id="ip"></span>
</div>
<div id="shareArea" style="display:none">
<h3>Share results</h3>
<p>Test ID: <span id="testId"></span></p>
<input type="text" value="" id="resultsURL" readonly="readonly" onclick="this.select();this.focus();this.select();document.execCommand('copy');alert('Link copied')"/>
<img src="" id="resultsImg" />
</div>
</div>
<a href="https://github.com/librespeed/speedtest">Source code</a>
</div>
<div id="privacyPolicy" style="display:none">
<h2>Privacy Policy</h2>
<p>This HTML5 speed test server is configured with telemetry enabled.</p>
<h4>What data we collect</h4>
<p>
At the end of the test, the following data is collected and stored:
<ul>
<li>Test ID</li>
<li>Time of testing</li>
<li>Test results (download and upload speed, ping and jitter)</li>
<li>IP address</li>
<li>ISP information</li>
<li>Approximate location (inferred from IP address, not GPS)</li>
<li>User agent and browser locale</li>
<li>Test log (contains no personal information)</li>
</ul>
</p>
<h4>How we use the data</h4>
<p>
Data collected through this service is used to:
<ul>
<li>Allow sharing of test results (sharable image for forums, etc.)</li>
<li>To improve the service offered to you (for instance, to detect problems on our side)</li>
</ul>
No personal information is disclosed to third parties.
</p>
<h4>Your consent</h4>
<p>
By starting the test, you consent to the terms of this privacy policy.
</p>
<h4>Data removal</h4>
<p>
If you want to have your information deleted, you need to provide either the ID of the test or your IP address. This is the only way to identify your data, without this information we won't be able to comply with your request.<br/><br/>
Contact this email address for all deletion requests: <a href="mailto:<?=getenv("EMAIL") ?>"><?=getenv("EMAIL") ?></a>.
</p>
<br/><br/>
<div class="closePrivacyPolicy">
<a class="privacy" href="#" onclick="I('privacyPolicy').style.display='none'">Close</a>
</div>
<br/>
</div>
</body>
</html>